yubihsm-3.1.1/COPYING0000644000000000000000000002613600000000000011126 0ustar00 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. yubihsm-3.1.1/NEWS0000644000000000000000000000446200000000000010570 0ustar00* Version 3.1.1 (released 2025-06-24) ** Support for compressing X509 certificates before import. ** Simplified parsing of extensions in attestation certificates. ** Changes relevant to maintainers *** Switched package manager to uv. *** Replaced Black, flake8 and bandit with ruff, and added pyright. * Version 3.1.0 (released 2024-09-09) ** Support for asymmetric wrap (for FW 2.4+). ** Support for wrapping ed25519 keys with seed (for FW 2.4+). ** Deprectaded `get_fips_mode` (use `get_fips_status` instead). ** Added `py.typed` for type checker compatibility. * Version 3.0.0 (released 2023-12-07) ** NOTE: Backwards incompatible release. ** Dropped Python 2 support, new minimum requirement: Python 3.8. ** Added type hints. ** Bumped minimum supported Cryptography version to 2.6. ** Dropped yubihsm.eddsa package, in favor of EdDSA support in Cryptography. ** Dropped custom constants for Brainpool curves, in favor of those in Cryptography. ** Dropped `.generated`, `.imported`, and `.wrapped` from ORIGIN. Instead use: `ORIGIN.GENERATED in origin`, etc. ** Added support for asymmetric authentication. ** Added support for symmetric encryption (AES). ** Changes relevant to maintainers: *** Added mypy to pre-commit checks. *** Switched build and packaging system to poetry. *** Switched to using pytest for testing (unittest still used in some places). * Version 2.1.2 (released 2022-12-05) ** Bugfix: Fix broken sign_ssh_certificate command. * Version 2.1.1 (released 2022-09-22) ** Dependency fix: Require Cryptography <38. * Version 2.1.0 (released 2021-04-13) ** Stop using deprecated functions from cryptography.io (prevents warnings). ** Support Prehashed data when signing. ** Implement context manager (python with-statement) for YubiHsm and AuthSession. ** Bugfix: Fix byte-order issue with AEAD nonce ID. * Version 2.0.1 (released 2019-06-19) ** Bugfix: ORIGIN representation was broken, causing get_info() to fail. ** Bugfix: Algorithm parsing in DeviceInfo fixed. ** Handing of too large messages improved. * Version 2.0.0 (released 2018-11-26) ** Published under the Apache v2.0 software license. ** Reworked most library APIs to align with SDK 2.0 changes. ** Added documentation to all public APIs, with Sphinx generated docs. * Version 1.0.0 (released 2017-10-27) ** First version yubihsm-3.1.1/README.adoc0000644000000000000000000001040000000000000011643 0ustar00== python-yubihsm Python library and tests for the YubiHSM 2. The current version (3.0) supports Python 3.9 and later. Communicates with the YubiHSM 2 connector daemon, which must already be running. It can also communicate directly with the YubiHSM 2 via USB (requires libusb). === License .... Copyright 2023 Yubico AB Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .... === Installation From PyPI: $ pip install yubihsm[http,usb] From a source .tar.gz: $ pip install yubihsm-.tar.gz[http,usb] Omitting a tag from the brackets will install the library without support for that backend, and will avoid installing unneeded dependencies. === Quick reference commands: [source,python] ---- from yubihsm import YubiHsm from yubihsm.defs import CAPABILITY, ALGORITHM from yubihsm.objects import AsymmetricKey from cryptography.hazmat.primitives import serialization # Connect to the YubiHSM via the connector using the default password: hsm = YubiHsm.connect('http://localhost:12345') session = hsm.create_session_derived(1, 'password') # Generate a private key on the YubiHSM for creating signatures: key = AsymmetricKey.generate( # Generate a new key object in the YubiHSM. session, # Secure YubiHsm session to use. 0, # Object ID, 0 to get one assigned. 'My key', # Label for the object. 1, # Domain(s) for the object. CAPABILITY.SIGN_ECDSA, # Capabilities for the object, can have multiple. ALGORITHM.EC_P256 # Algorithm for the key. ) # pub_key is a cryptography.io ec.PublicKey, see https://cryptography.io pub_key = key.get_public_key() # Write the public key to a file: with open('public_key.pem', 'wb') as f: f.write(pub_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo )) # Sign some data: signature = key.sign_ecdsa(b'Hello world!') # Create a signature. # Clean up: session.close() hsm.close() ---- === Development This project uses https://docs.astral.sh/uv/[uv] for development. Follow the uv Getting Started guide to install and configure it. When `uv` is installed and configured you can set up the dev environment for this project by running the following command in the root directory of the repository: $ uv sync --all-extras ==== Pre-commit checks This project uses https://pre-commit.com to run several checks on the code prior to committing. To enable the hooks, run these commands in the root directory of the repository: $ uv tool install pre-commit $ pre-commit install Once the hooks are installed, they will run automatically on any changed files when committing. To run the hooks against all files in the repository, run: $ pre-commit run --all-files ==== Running tests Running the tests require a YubiHSM2 to run against, with the default authentication key enabled (as is the case after performing a factory reset). WARNING: The YubiHSM under test will be factory reset by the tests! $ uv run pytest See pytest documentation for instructions on running a specific test. By default the tests will connect to a yubihsm-connector running with the default settings on http://localhost:12345. To change this, use the `--backend` argument, eg: $ uv run pytest --backend "yhusb://" Access to the device requires proper permissions, so either use sudo or setup a udev rule. Sample udev configuration can be found link:https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-connector/[here]. ==== Generating HTML documentation To build the HTML documentation, run: $ uv run make -C docs/ html The resulting output will be in docs/_build/html/. ==== Source releases for distribution Build a source release: $ uv build The resulting .tar.gz and .whl will be created in `dist/`. yubihsm-3.1.1/pyproject.toml0000644000000000000000000000274100000000000013003 0ustar00[project] name = "yubihsm" version = "3.1.1" description = "Library for communication with a YubiHSM 2 over HTTP or USB." authors = [{ name = "Dain Nilsson", email = "" }] readme = "README.adoc" requires-python = ">=3.9" license = { file = "COPYING" } classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Topic :: Security :: Cryptography", "Topic :: Software Development :: Libraries" ] dependencies = ["cryptography (>=2.6, <47)"] [dependency-groups] dev = [ "pytest>=8.4.1", "sphinx>=8.1.3 ; python_full_version >= '3.10'", "sphinx-autoapi>=3.6.0 ; python_full_version >= '3.10'", "sphinx-rtd-theme>=3.0.2 ; python_full_version >= '3.10'", ] [project.optional-dependencies] http = ["requests (>=2.0, <3.0)"] usb = ["pyusb (>=1.0, <2.0)"] [project.urls] Homepage = "https://developers.yubico.com/YubiHSM2/" Repository = "https://github.com/Yubico/python-yubihsm" [tool.poetry] include = [ { path = "COPYING", format = "sdist" }, { path = "NEWS", format = "sdist" }, { path = "README.adoc", format = "sdist" }, "tests/", ] [build-system] requires = ["poetry-core>=2.0"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] testpaths = ["tests"] [tool.ruff.lint] select = ["I", "S"] exclude = ["tests/*"] [tool.pyright] venvPath = "." venv = ".venv" exclude = ["tests/", "docs/", "examples/"]yubihsm-3.1.1/tests/__init__.py0000644000000000000000000000110200000000000013330 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. yubihsm-3.1.1/tests/conftest.py0000644000000000000000000000016200000000000013423 0ustar00def pytest_addoption(parser): parser.addoption("--backend", action="store", default="http://localhost:12345") yubihsm-3.1.1/tests/device/__init__.py0000644000000000000000000000113500000000000014575 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. DEFAULT_KEY = "password" yubihsm-3.1.1/tests/device/conftest.py0000644000000000000000000000306000000000000014662 0ustar00from yubihsm import YubiHsm from yubihsm.exceptions import YubiHsmDeviceError from time import sleep from functools import partial from . import DEFAULT_KEY import pytest from typing import List @pytest.fixture(scope="session") def connect_hsm(pytestconfig): backend_uri = pytestconfig.getoption("backend") return partial(YubiHsm.connect, backend_uri) @pytest.fixture(scope="module") def hsm(connect_hsm): with connect_hsm() as hsm: yield hsm @pytest.fixture(scope="module") def info(hsm): return hsm.get_device_info() @pytest.fixture(scope="module") def session(hsm): with hsm.create_session_derived(1, DEFAULT_KEY) as session: yield session _logged_version: List[bool] = [] @pytest.fixture(scope="module", autouse=True) def _hsm_info(info, session, request): if not _logged_version: # Run only once name = "YubiHSM " try: session.get_fips_status() name += "FIPS " except YubiHsmDeviceError: pass name += "v" + (".".join(str(v) for v in info.version)) capmanager = request.config.pluginmanager.getplugin("capturemanager") with capmanager.global_and_fixture_disabled(): print() print() print("ℹ️ Running tests on", name) print() _logged_version.append(True) @pytest.fixture(autouse=True, scope="session") def _reset_hsm(connect_hsm): with connect_hsm() as hsm: with hsm.create_session_derived(1, DEFAULT_KEY) as session: session.reset_device() sleep(5.0) yubihsm-3.1.1/tests/device/test_aes.py0000644000000000000000000001055100000000000014647 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY from yubihsm.objects import SymmetricKey from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from typing import Optional import os import pytest AES_ALGORITHMS = (ALGORITHM.AES128, ALGORITHM.AES192, ALGORITHM.AES256) AES_CAPABILITIES = ( CAPABILITY.ENCRYPT_ECB | CAPABILITY.DECRYPT_ECB | CAPABILITY.ENCRYPT_CBC | CAPABILITY.DECRYPT_CBC ) @pytest.fixture(autouse=True, scope="module") def prerequisites(info): if info.version < (2, 3, 0): pytest.skip("Symmetric keys require YubiHSM 2.3.0") @pytest.fixture(scope="module", params=AES_ALGORITHMS) def generated_key(session, request): algorithm = request.param key = SymmetricKey.generate( session, 0, "Generated AES Key %x" % algorithm, 0xFFFF, AES_CAPABILITIES, algorithm, ) yield key key.delete() @pytest.fixture(scope="module", params=AES_ALGORITHMS) def imported_key(session, request): algorithm = request.param key_to_import = os.urandom(algorithm.to_key_size()) key = SymmetricKey.put( session, 0, "Imported AES Key %x" % algorithm, 0xFFFF, AES_CAPABILITIES, algorithm, key_to_import, ) yield key, key_to_import key.delete() def test_import_invalid_key_size(session): # Key length must match algorithm with pytest.raises(ValueError): SymmetricKey.put( session, 0, "Test PUT invalid key length", 0xFFFF, AES_CAPABILITIES, ALGORITHM.AES128, os.urandom(24), ) def test_import_invalid_algorithm(session): # Algorithm must be AES128, AES192 or AES256 with pytest.raises(ValueError): SymmetricKey.put( session, 0, "Test PUT invalid algorithm", 0xFFFF, AES_CAPABILITIES, ALGORITHM.AES128_CCM_WRAP, os.urandom(16), ) class TestSymmetricECB: def validate_ecb( self, pt: bytes, keyobj: SymmetricKey, key: Optional[bytes] = None ): ct = keyobj.encrypt_ecb(pt) if key: encryptor = Cipher(algorithms.AES(key), modes.ECB()).encryptor() assert ct == encryptor.update(pt) + encryptor.finalize() assert pt == keyobj.decrypt_ecb(ct) def test_ecb_generated_key(self, generated_key): pt = os.urandom(256) self.validate_ecb(pt, generated_key) def test_ecb_imported_key(self, imported_key): pt = os.urandom(256) self.validate_ecb(pt, *imported_key) def test_ecb_large_pt_generated_key(self, generated_key): pt = os.urandom(4096) self.validate_ecb(pt, generated_key) def test_ecb_large_pt_imported_key(self, imported_key): pt = os.urandom(4096) self.validate_ecb(pt, *imported_key) class TestSymmetricCBC: def validate_cbc( self, pt: bytes, keyobj: SymmetricKey, key: Optional[bytes] = None ): iv = os.urandom(16) ct = keyobj.encrypt_cbc(iv, pt) if key: encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor() assert ct == encryptor.update(pt) + encryptor.finalize() assert pt == keyobj.decrypt_cbc(iv, ct) def test_cbc_generated_key(self, generated_key): pt = os.urandom(256) self.validate_cbc(pt, generated_key) def test_cbc_imported_key(self, imported_key): pt = os.urandom(256) self.validate_cbc(pt, *imported_key) def test_cbc_large_pt_generated_key(self, generated_key): pt = os.urandom(4096) self.validate_cbc(pt, generated_key) def test_cbc_large_pt_imported_key(self, imported_key): pt = os.urandom(4096) self.validate_cbc(pt, *imported_key) yubihsm-3.1.1/tests/device/test_attestation.py0000644000000000000000000001337100000000000016441 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY, FIPS_STATUS, OBJECT from yubihsm.objects import AsymmetricKey, AttestationExtensions from yubihsm.exceptions import YubiHsmDeviceError from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding from cryptography.hazmat.primitives.serialization import Encoding from time import sleep import datetime import uuid import pytest def create_pair(session, algorithm): if algorithm == ALGORITHM.RSA_2048: private_key = rsa.generate_private_key(0x10001, 2048, default_backend()) elif algorithm == ALGORITHM.RSA_3072: private_key = rsa.generate_private_key(0x10001, 3072, default_backend()) elif algorithm == ALGORITHM.RSA_4096: private_key = rsa.generate_private_key(0x10001, 4096, default_backend()) else: ec_curve = ALGORITHM.to_curve(algorithm) private_key = ec.generate_private_key(ec_curve, default_backend()) builder = x509.CertificateBuilder() name = x509.Name( [x509.NameAttribute(NameOID.COMMON_NAME, "Test Attestation Certificate")] ) builder = builder.subject_name(name) builder = builder.issuer_name(name) one_day = datetime.timedelta(1, 0, 0) builder = builder.not_valid_before(datetime.datetime.today() - one_day) builder = builder.not_valid_after(datetime.datetime.today() + one_day) builder = builder.serial_number(int(uuid.uuid4())) builder = builder.public_key(private_key.public_key()) certificate = builder.sign(private_key, hashes.SHA256(), default_backend()) attkey = AsymmetricKey.put( session, 0, "Test Create Pair", 0xFFFF, CAPABILITY.SIGN_ATTESTATION_CERTIFICATE, private_key, ) certobj = attkey.put_certificate( "Test Create Pair", 0xFFFF, CAPABILITY.NONE, certificate ) assert certificate.public_bytes(Encoding.DER) == certobj.get() return attkey, certobj, certificate ASYM_ALGOS = [ ALGORITHM.RSA_2048, ALGORITHM.RSA_3072, ALGORITHM.RSA_4096, ALGORITHM.EC_P256, ALGORITHM.EC_P384, ALGORITHM.EC_P521, ALGORITHM.EC_K256, ALGORITHM.EC_P224, ] class TestAttestationAlgorithms: @pytest.fixture(scope="class", params=ASYM_ALGOS) def generated_key(self, request, session): algorithm = request.param key = AsymmetricKey.generate( session, 0, "Test Attestation %x" % algorithm, 0xFFFF, CAPABILITY.NONE, algorithm, ) yield key key.delete() @pytest.mark.parametrize("algorithm", ASYM_ALGOS) def test_attestation(self, session, generated_key, algorithm, info): attkey, attcertobj, attcert = create_pair(session, algorithm) pubkey = attcert.public_key() # Verify signatures cert = generated_key.attest(attkey.id) data = cert.tbs_certificate_bytes if isinstance(pubkey, rsa.RSAPublicKey): pubkey.verify( cert.signature, data, padding.PKCS1v15(), cert.signature_hash_algorithm, ) else: pubkey.verify(cert.signature, data, ec.ECDSA(cert.signature_hash_algorithm)) # Verify certificate extensions ext = AttestationExtensions.parse(cert) assert info.version == ext.firmware_version assert info.serial == ext.serial obj = generated_key.get_info() assert obj.origin == ext.origin assert obj.domains == ext.domains assert obj.capabilities == ext.capabilities assert obj.id == ext.object_id assert obj.label == ext.label # Verify correct public key assert cert.public_key() == generated_key.get_public_key() # Clean up attkey.delete() attcertobj.delete() def test_fips_approved_attestation(session, connect_hsm): try: session.get_fips_status() except YubiHsmDeviceError: pytest.skip("Non-FIPS YubiHSM") try: # Configure into FIPS approved mode session.reset_device() sleep(5.0) hsm = connect_hsm() new_session = hsm.create_session_derived(1, "password") new_session.set_fips_mode(True) assert new_session.get_fips_status() == FIPS_STATUS.PENDING # Change the default auth key authkey = new_session.get_object(1, OBJECT.AUTHENTICATION_KEY) authkey.change_password("password2") assert new_session.get_fips_status() == FIPS_STATUS.ON # Generate keys key = AsymmetricKey.generate( new_session, 0, "Test FIPS Attestation", 0xFFFF, CAPABILITY.NONE, ALGORITHM.RSA_2048, ) attkey, attcertobj, attcert = create_pair(new_session, ALGORITHM.RSA_2048) cert = key.attest(attkey.id) ext = AttestationExtensions.parse(cert) assert ext.fips_approved in (True, None) finally: # Reset device to get out of FIPS approved mode new_session.reset_device() sleep(5.0) yubihsm-3.1.1/tests/device/test_auth.py0000644000000000000000000002005000000000000015033 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import COMMAND, CAPABILITY, ERROR from yubihsm.objects import AuthenticationKey from yubihsm.exceptions import YubiHsmAuthenticationError, YubiHsmDeviceError from yubihsm.utils import password_to_key from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec from binascii import a2b_hex import pytest import os class TestAuthenticationKey: def test_put_unicode_authkey(self, hsm, session): # UTF-8 encoded unicode password password = b"\xf0\x9f\x98\x81\xf0\x9f\x98\x83\xf0\x9f\x98\x84".decode() authkey = AuthenticationKey.put_derived( session, 0, "Test PUT authkey", 1, CAPABILITY.NONE, CAPABILITY.NONE, password, ) with hsm.create_session_derived(authkey.id, password) as session: message = os.urandom(256) resp = session.send_secure_cmd(COMMAND.ECHO, message) assert resp == message authkey.delete() class TestChangeAuthenticationKey: @pytest.fixture(autouse=True) def prerequisites(self, info): if info.version < (2, 1, 0): pytest.skip("Change authentication key requires 2.1.0") def test_change_password(self, hsm, session): # Create an auth key with the capability to change authkey = AuthenticationKey.put_derived( session, 0, "Test CHANGE authkey", 1, CAPABILITY.CHANGE_AUTHENTICATION_KEY, CAPABILITY.NONE, "first_password", ) # Can't change the password of another key with pytest.raises(YubiHsmDeviceError) as context: authkey.change_password("second_password") assert context.value.code == ERROR.INVALID_ID # Try again, using the new auth key with hsm.create_session_derived(authkey.id, "first_password") as session: authkey.with_session(session).change_password("second_password") with pytest.raises(YubiHsmAuthenticationError): hsm.create_session_derived(authkey.id, "first_password") hsm.create_session_derived(authkey.id, "second_password").close() authkey.delete() with pytest.raises(YubiHsmDeviceError) as context: hsm.create_session_derived(authkey.id, "second_password") assert context.value.code == ERROR.OBJECT_NOT_FOUND def test_change_raw_keys(self, session, hsm): key_enc = a2b_hex("090b47dbed595654901dee1cc655e420") key_mac = a2b_hex("592fd483f759e29909a04c4505d2ce0a") # Create an auth key with the capability to change authkey = AuthenticationKey.put( session, 0, "Test CHANGE authkey", 1, CAPABILITY.CHANGE_AUTHENTICATION_KEY, CAPABILITY.NONE, key_enc, key_mac, ) with hsm.create_session_derived(authkey.id, "password") as session: key_enc, key_mac = password_to_key("second_password") authkey.with_session(session).change_key(key_enc, key_mac) with hsm.create_session_derived(authkey.id, "second_password"): pass authkey.delete() class TestSessions: def test_parallel_sessions(self, session, hsm): authkey1 = AuthenticationKey.put_derived( session, 0, "Test authkey 1", 1, CAPABILITY.NONE, CAPABILITY.NONE, "one", ) authkey2 = AuthenticationKey.put_derived( session, 0, "Test authkey 2", 2, CAPABILITY.NONE, CAPABILITY.NONE, "two", ) authkey3 = AuthenticationKey.put_derived( session, 0, "Test authkey 3", 1, CAPABILITY.NONE, CAPABILITY.NONE, "three", ) session1 = hsm.create_session_derived(authkey1.id, "one") session2 = hsm.create_session_derived(authkey2.id, "two") session3 = hsm.create_session_derived(authkey3.id, "three") session2.close() session1.send_secure_cmd(COMMAND.ECHO, b"hello") session3.send_secure_cmd(COMMAND.ECHO, b"hi") session1.send_secure_cmd(COMMAND.ECHO, b"hello") session3.send_secure_cmd(COMMAND.ECHO, b"greetings") session1.close() session3.send_secure_cmd(COMMAND.ECHO, b"good bye") session3.close() authkey1.delete() authkey2.delete() authkey3.delete() class TestAymmetricAuthenticationKey: @pytest.fixture(autouse=True) def prerequisites(self, info): if info.version < (2, 3, 0): pytest.skip("Asymmetric authentication requires 2.3.0") def test_put_public_key(self, hsm, session): private_key = ec.generate_private_key(ec.SECP256R1(), backend=default_backend()) authkey = AuthenticationKey.put_public_key( session, 0, "Test PUT asym authkey", 1, CAPABILITY.NONE, CAPABILITY.NONE, private_key.public_key(), ) try: with hsm.create_session_asymmetric( authkey.id, private_key ) as asymmetric_session: message = os.urandom(256) resp = asymmetric_session.send_secure_cmd(COMMAND.ECHO, message) assert message == resp finally: authkey.delete() def test_change_public_key(self, hsm, session): first_private_key = ec.generate_private_key( ec.SECP256R1(), backend=default_backend() ) second_private_key = ec.generate_private_key( ec.SECP256R1(), backend=default_backend() ) authkey = AuthenticationKey.put_public_key( session, 0, "Test PUT asym authkey", 1, CAPABILITY.CHANGE_AUTHENTICATION_KEY, CAPABILITY.NONE, first_private_key.public_key(), ) with hsm.create_session_asymmetric( authkey.id, first_private_key ) as asymmetric_session: authkey.with_session(asymmetric_session).change_public_key( second_private_key.public_key() ) try: with hsm.create_session_asymmetric( authkey.id, second_private_key ) as asymmetric_session: message = os.urandom(256) resp = asymmetric_session.send_secure_cmd(COMMAND.ECHO, message) assert message == resp finally: authkey.delete() def test_cached_device_public_key(self, hsm, session): private_key = ec.generate_private_key(ec.SECP256R1(), backend=default_backend()) authkey = AuthenticationKey.put_public_key( session, 0, "Test PUT asym authkey", 1, CAPABILITY.NONE, CAPABILITY.NONE, private_key.public_key(), ) right_public_key = hsm.get_device_public_key() wrong_public_key = ec.generate_private_key( ec.SECP256R1(), backend=default_backend() ).public_key() with pytest.raises(YubiHsmAuthenticationError): hsm.create_session_asymmetric(authkey.id, private_key, wrong_public_key) try: hsm.create_session_asymmetric(authkey.id, private_key, right_public_key) finally: authkey.delete() yubihsm-3.1.1/tests/device/test_basic.py0000644000000000000000000002050400000000000015157 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY, OBJECT, COMMAND, ORIGIN, FIPS_STATUS from yubihsm.objects import ( YhsmObject, AsymmetricKey, HmacKey, WrapKey, AuthenticationKey, ) from cryptography.hazmat.primitives.asymmetric import ec from yubihsm.exceptions import YubiHsmInvalidRequestError, YubiHsmDeviceError from time import sleep import uuid import os import pytest class TestListObjects: def print_list_objects(self, session): objlist = session.list_objects() for i in range(len(objlist)): print( "id: ", "0x%0.4X" % objlist[i].id, ",type: ", objlist[i].object_type.name, "\t,sequence: ", objlist[i].sequence, ) objinfo = objlist[1].get_info() print( "id: ", "0x%0.4X" % objinfo.id, ",type: ", objinfo.object_type.name, "\t,sequence: ", objinfo.sequence, ",domains: 0x%0.4X" % objinfo.domains, ",capabilities: 0x%0.8X" % objinfo.capabilities, ",algorithm: ", objinfo.algorithm, ) def key_in_list(self, session, keytype, algorithm=None): dom = None cap = CAPABILITY.NONE key_label = "%s%s" % (str(uuid.uuid4()), b"\xf0\x9f\x98\x83".decode()) key: YhsmObject if keytype == OBJECT.ASYMMETRIC_KEY: dom = 0xFFFF key = AsymmetricKey.generate(session, 0, key_label, dom, cap, algorithm) elif keytype == OBJECT.WRAP_KEY: dom = 0x01 key = WrapKey.generate(session, 0, key_label, dom, cap, algorithm, cap) elif keytype == OBJECT.HMAC_KEY: dom = 0x01 key = HmacKey.generate(session, 0, key_label, dom, cap, algorithm) elif keytype == OBJECT.AUTHENTICATION_KEY: dom = 0x01 key = AuthenticationKey.put_derived( session, 0, key_label, dom, cap, cap, "password", ) objlist = session.list_objects(object_id=key.id, object_type=key.object_type) assert objlist[0].id == key.id assert objlist[0].object_type == key.object_type objinfo = objlist[0].get_info() assert objinfo.id == key.id assert objinfo.object_type == key.object_type assert objinfo.domains == dom assert objinfo.capabilities == cap if algorithm: assert objinfo.algorithm == algorithm if key.object_type == OBJECT.AUTHENTICATION_KEY: assert objinfo.origin == ORIGIN.IMPORTED else: assert objinfo.origin == ORIGIN.GENERATED assert objinfo.label == key_label key.delete() def test_keys_in_list(self, session): self.key_in_list(session, OBJECT.ASYMMETRIC_KEY, ALGORITHM.EC_P256) self.key_in_list(session, OBJECT.WRAP_KEY, ALGORITHM.AES128_CCM_WRAP) self.key_in_list(session, OBJECT.HMAC_KEY, ALGORITHM.HMAC_SHA1) self.key_in_list(session, OBJECT.AUTHENTICATION_KEY) def test_list_all_params(self, session): # TODO: this test should check for presence of some things.. session.list_objects( object_id=1, object_type=OBJECT.HMAC_KEY, domains=1, capabilities=CAPABILITY.ALL, algorithm=ALGORITHM.HMAC_SHA1, label="foo", ) class TestVarious: def test_device_info(self, hsm): device_info = hsm.get_device_info() assert len(device_info.version) == 3 assert device_info.serial > 0 assert device_info.log_used > 0 assert device_info.log_size >= device_info.log_used assert len(device_info.supported_algorithms) >= 47 if device_info.version > (2, 4, 0): assert isinstance(device_info.part_number, str) def test_get_pseudo_random(self, session): data = session.get_pseudo_random(10) assert len(data) == 10 data2 = session.get_pseudo_random(10) assert len(data2) == 10 assert data != data2 def test_send_too_big(self, hsm, session): max_msg_size = hsm._msg_buf_size - 1 buf = os.urandom(max_msg_size - 3 + 1) # Message 1 byte too large with pytest.raises(YubiHsmInvalidRequestError): hsm.send_cmd(COMMAND.ECHO, buf) class TestDevicePublicKey: @pytest.fixture(autouse=True) def prerequisites(self, info): if info.version < (2, 3, 0): pytest.skip("Device public keys requires 2.3.0") def test_get_device_public_key(self, hsm): public_key = hsm.get_device_public_key() assert isinstance(public_key, ec.EllipticCurvePublicKey) class TestEcho: def plain_echo(self, hsm, echo_len): echo_buf = os.urandom(echo_len) resp = hsm.send_cmd(COMMAND.ECHO, echo_buf) assert len(resp) == echo_len assert resp == echo_buf def secure_echo(self, session, echo_len): echo_buf = os.urandom(echo_len) resp = session.send_secure_cmd(COMMAND.ECHO, echo_buf) assert resp == echo_buf def test_plain_echo(self, hsm): self.plain_echo(hsm, 1024) def test_secure_echo(self, session): self.secure_echo(session, 1024) def test_plain_echo_many(self, hsm): for i in range(1, 256): self.plain_echo(hsm, i) def test_echo_max_size(self, hsm, session): self.plain_echo(hsm, 2021) self.secure_echo(session, 2021) class TestFipsOptions: @pytest.fixture(scope="class", autouse=True) def session2(self, session, connect_hsm): try: session.get_fips_status() session.reset_device() sleep(5.0) hsm = connect_hsm() new_session = hsm.create_session_derived(1, "password") yield new_session new_session.reset_device() sleep(5.0) except YubiHsmDeviceError: pytest.skip("Non-FIPS YubiHSM") def test_set_in_fips_mode(self, session2, info): assert session2.get_fips_status() == FIPS_STATUS.OFF session2.set_fips_mode(True) if info.version < (2, 4, 0): assert session2.get_fips_status() == FIPS_STATUS.ON else: assert session2.get_fips_status() == FIPS_STATUS.PENDING def test_fips_mode_disables_algorithms(self, session2, info): session2.set_fips_mode(True) enabled = session2.get_enabled_algorithms() if info.version < (2, 4, 0): assert not any( enabled[alg] for alg in ( ALGORITHM.RSA_PKCS1_SHA1, ALGORITHM.RSA_PSS_SHA1, ALGORITHM.EC_ECDSA_SHA1, ALGORITHM.EC_ED25519, ) ) else: assert not any( enabled[alg] for alg in ( ALGORITHM.RSA_PKCS1_SHA1, ALGORITHM.RSA_PSS_SHA1, ALGORITHM.EC_K256, ALGORITHM.EC_ECDSA_SHA1, ALGORITHM.RSA_PKCS1_DECRYPT, ) ) def test_enabling_algorithms_in_fips_mode(self, session2, info): session2.set_fips_mode(True) if info.version < (2, 4, 0): # For YubiHSM FW < 2.4.0, enabling dissallowed algorithms # disables FIPS mode. session2.set_enabled_algorithms( { ALGORITHM.RSA_PKCS1_SHA1: True, } ) assert session2.get_fips_status() == FIPS_STATUS.OFF else: with pytest.raises(YubiHsmDeviceError): session2.set_enabled_algorithms({ALGORITHM.RSA_PKCS1_SHA1: True}) yubihsm-3.1.1/tests/device/test_delete.py0000644000000000000000000000751200000000000015344 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY, ERROR from yubihsm.objects import ( AuthenticationKey, HmacKey, Opaque, AsymmetricKey, OtpAeadKey, WrapKey, SymmetricKey, ) from yubihsm.exceptions import YubiHsmDeviceError from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.backends import default_backend import os import pytest def _set_up_key(hsm, session, capability): password = session.get_pseudo_random(32).hex() key = AuthenticationKey.put_derived( session, 0, "Test Delete authkey", 1, capability, CAPABILITY.NONE, password ) session = hsm.create_session_derived(key.id, password) return key, session def _test_delete(hsm, session, obj, capability): pos_key, pos_sess = _set_up_key(hsm, session, capability) neg_key, neg_sess = _set_up_key(hsm, session, CAPABILITY.NONE) with pytest.raises(YubiHsmDeviceError) as context: obj.with_session(neg_sess).delete() assert context.value.code == ERROR.INSUFFICIENT_PERMISSIONS obj.with_session(pos_sess).delete() pos_sess.close() neg_sess.close() neg_key.delete() pos_key.delete() def test_opaque(hsm, session): obj = Opaque.put( session, 0, "Test opaque data", 1, CAPABILITY.NONE, ALGORITHM.OPAQUE_DATA, b"data", ) _test_delete(hsm, session, obj, CAPABILITY.DELETE_OPAQUE) def test_authentication_key(hsm, session): obj = AuthenticationKey.put_derived( session, 0, "Test delete authkey", 1, CAPABILITY.GET_LOG_ENTRIES, CAPABILITY.NONE, session.get_pseudo_random(32).hex(), ) _test_delete(hsm, session, obj, CAPABILITY.DELETE_AUTHENTICATION_KEY) def test_asymmetric_key(hsm, session): obj = AsymmetricKey.put( session, 0, "Test delete asym", 0xFFFF, CAPABILITY.SIGN_ECDSA, ec.generate_private_key(ec.SECP384R1(), backend=default_backend()), ) _test_delete(hsm, session, obj, CAPABILITY.DELETE_ASYMMETRIC_KEY) def test_wrap_key(hsm, session): obj = WrapKey.put( session, 0, "Test delete", 1, CAPABILITY.IMPORT_WRAPPED, ALGORITHM.AES192_CCM_WRAP, CAPABILITY.NONE, os.urandom(24), ) _test_delete(hsm, session, obj, CAPABILITY.DELETE_WRAP_KEY) def test_hmac_key(hsm, session): obj = HmacKey.put( session, 0, "Test delete HMAC", 1, CAPABILITY.SIGN_HMAC, bytes(16) ) _test_delete(hsm, session, obj, CAPABILITY.DELETE_HMAC_KEY) def test_otp_aead_key(hsm, session): obj = OtpAeadKey.put( session, 0, "Test delete OTP AEAD", 1, CAPABILITY.DECRYPT_OTP, ALGORITHM.AES256_YUBICO_OTP, 0x00000001, os.urandom(32), ) _test_delete(hsm, session, obj, CAPABILITY.DELETE_OTP_AEAD_KEY) def test_symmetric_key(hsm, session, info): if info.version < (2, 3, 0): pytest.skip("Symmetric keys require YubiHSM 2.3.0") obj = SymmetricKey.put( session, 0, "Test delete symmetric", 0xFFFF, CAPABILITY.DECRYPT_ECB, ALGORITHM.AES128, os.urandom(16), ) _test_delete(hsm, session, obj, CAPABILITY.DELETE_SYMMETRIC_KEY) yubihsm-3.1.1/tests/device/test_ec.py0000644000000000000000000003572400000000000014477 0ustar00# coding=utf-8 # Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY, COMMAND, ERROR from yubihsm.objects import AsymmetricKey from yubihsm.exceptions import YubiHsmDeviceError from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, ed25519, utils as crypto_utils from binascii import a2b_hex from enum import Enum import os import struct import pytest class Mode(Enum): IMPORT = 0 GENERATE = 1 def __str__(self): return self.name ECDSA_CURVES = [ ec.SECP224R1, ec.SECP256R1, ec.SECP256K1, ec.SECP384R1, ec.SECP521R1, ec.BrainpoolP256R1, ec.BrainpoolP384R1, ec.BrainpoolP512R1, ] HASHES = [ hashes.SHA1, hashes.SHA256, hashes.SHA384, hashes.SHA512, ] @pytest.fixture(params=[Mode.IMPORT, Mode.GENERATE]) def keypair(request, session, curve): if request.param == Mode.GENERATE: asymkey = AsymmetricKey.generate( session, 0, "Generate EC", 0xFFFF, CAPABILITY.SIGN_ECDSA | CAPABILITY.DERIVE_ECDH, ALGORITHM.for_curve(curve()), ) public_key = asymkey.get_public_key() else: key = ec.generate_private_key(curve(), backend=default_backend()) asymkey = AsymmetricKey.put( session, 0, "SECP ECDSA Sign Sign", 0xFFFF, CAPABILITY.SIGN_ECDSA | CAPABILITY.DERIVE_ECDH, key, ) public_key = key.public_key() assert public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) == asymkey.get_public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) yield asymkey, public_key asymkey.delete() @pytest.mark.parametrize("hashtype", HASHES) @pytest.mark.parametrize("curve", ECDSA_CURVES) def test_ecdsa_sign(info, session, keypair, curve, hashtype): asymkey, public_key = keypair data = os.urandom(64) if info.version < (2, 1, 0): # Manual truncation needed length = min(curve.key_size // 8, hashtype.digest_size) resp = asymkey.sign_ecdsa(data, hash=hashtype(), length=length) else: resp = asymkey.sign_ecdsa(data, hash=hashtype()) public_key.verify(resp, data, ec.ECDSA(hashtype())) @pytest.mark.parametrize("curve", ECDSA_CURVES) def test_derive_ecdh(session, keypair, curve): asymkey, public_key = keypair ekey = ec.generate_private_key(curve(), backend=default_backend()) secret = ekey.exchange(ec.ECDH(), public_key) resp = asymkey.derive_ecdh(ekey.public_key()) assert secret == resp def test_bad_ecdh_keys(session): pubkeys = [ # this is a public key not on the curve (p256) "04cdeb39edd03e2b1a11a5e134ec99d5f25f21673d403f3ecb47bd1fa676638958ea58493b8429598c0b49bbb85c3303ddb1553c3b761c2caacca71606ba9ebaca", # noqa E501 # all zeroes public key "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", # noqa E501 # all ff public key "04ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", # noqa E501 ] key = AsymmetricKey.generate( session, 0, "badkey ecdh test", 0xFFFF, CAPABILITY.DERIVE_ECDH, ALGORITHM.EC_P256, ) keyid = struct.pack("!H", key.id) for pubkey in pubkeys: with pytest.raises(YubiHsmDeviceError) as context: session.send_secure_cmd(COMMAND.DERIVE_ECDH, keyid + a2b_hex(pubkey)) assert context.value.code == ERROR.INVALID_DATA key.delete() def test_biased_k(session): # n is the order of the p256r1 curve. n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 key = ec.generate_private_key(ec.SECP256R1(), backend=default_backend()) d = key.private_numbers().private_value asymkey = AsymmetricKey.put( session, 0, "Test ECDSA K", 0xFFFF, CAPABILITY.SIGN_ECDSA, key ) data = b"Hello World!" digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest.update(data) h = int.from_bytes(digest.finalize(), "big") # The assumption here is that for 1024 runs we should get a distribution # where each single bit is set between 400 and 1024 - 400 times. count = 1024 mincount = 400 bits = [0] * 256 for i in range(0, count): resp = asymkey.sign_ecdsa(data, hash=hashes.SHA256()) # Extract random number k from signature: # k = s^(-1) * (h + d*r) mod n (r, s) = crypto_utils.decode_dss_signature(resp) # Fermat's little theorem: a^(p-1) ≡ 1 (mod p), when p is prime. # s * s^(p-2) ≡ 1 (mod p) s_inv = pow(s, n - 2, n) k = s_inv * (h + d * r) % n for j in range(0, 256): if (k >> j) & 1: bits[j] += 1 for bit in bits: assert mincount < bit < count - mincount asymkey.delete() EDDSA_VECTORS = [ { "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 "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 "msg": b"", "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 }, { "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 "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 "msg": b"\x72", "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 }, { "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 "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 "msg": b"\xaf\x82", "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 }, { "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 "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 "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 "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 }, { "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 "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 "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 "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 }, ] @pytest.mark.parametrize("vector", EDDSA_VECTORS) def test_eddsa_vectors(session, vector): key = ed25519.Ed25519PrivateKey.from_private_bytes(vector["key"]) k = AsymmetricKey.put( session, 0, "Test Ed25519", 0xFFFF, CAPABILITY.SIGN_EDDSA, key ) assert ( k.get_public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ) == vector["pubkey"] ) assert k.sign_eddsa(vector["msg"]) == vector["sig"] k.delete() @pytest.fixture(params=[Mode.IMPORT, Mode.GENERATE]) def eddsa_keypair(request, session): if request.param == Mode.GENERATE: key = None asymkey = AsymmetricKey.generate( session, 0, "Generate EC", 0xFFFF, CAPABILITY.SIGN_EDDSA, ALGORITHM.EC_ED25519, ) public_key = asymkey.get_public_key() else: key = ed25519.Ed25519PrivateKey.generate() asymkey = AsymmetricKey.put( session, 0, "Test Ed25519", 0xFFFF, CAPABILITY.SIGN_EDDSA, key ) public_key = key.public_key() assert public_key.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ) == asymkey.get_public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ) yield asymkey, public_key, key asymkey.delete() @pytest.mark.parametrize("length", [128, 129, 2019]) def test_eddsa_sign(session, eddsa_keypair, length): asymkey, public_key, private_key = eddsa_keypair data = os.urandom(length) sig = asymkey.sign_eddsa(data) public_key.verify(sig, data) if private_key: # Imported key, compare to SW signature assert sig == private_key.sign(data) yubihsm-3.1.1/tests/device/test_hmac.py0000644000000000000000000002231000000000000015003 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY from yubihsm.objects import HmacKey import random import os import pytest @pytest.mark.parametrize( "algorithm, expect_len", [ (ALGORITHM.HMAC_SHA1, 20), (ALGORITHM.HMAC_SHA256, 32), (ALGORITHM.HMAC_SHA384, 48), (ALGORITHM.HMAC_SHA512, 64), ], ) def test_generate_hmac(session, algorithm, expect_len): caps = CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC hmackey = HmacKey.generate(session, 0, "Generate HMAC", 1, caps, algorithm) data = os.urandom(64) resp = hmackey.sign_hmac(data) assert len(resp) == expect_len assert hmackey.verify_hmac(resp, data) resp2 = hmackey.sign_hmac(data) assert len(resp2) == expect_len assert resp == resp2 data = os.urandom(64) resp2 = hmackey.sign_hmac(data) assert len(resp2) == expect_len assert resp != resp2 assert hmackey.verify_hmac(resp2, data) hmackey.delete() hmackey = HmacKey.generate(session, 0, "Generate HMAC", 1, caps, algorithm) resp = hmackey.sign_hmac(data) assert len(resp) == expect_len assert resp != resp2 assert hmackey.verify_hmac(resp, data) hmackey.delete() @pytest.mark.parametrize( "vector", [ { "key": b"\x0b" * 20, "chal": b"Hi There", "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 "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 "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 "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 }, { "key": b"Jefe", "chal": b"what do ya want for nothing?", "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 "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 "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 "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 }, { "key": b"\xaa" * 20, "chal": b"\xdd" * 50, "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 "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 "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 "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 }, { "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 "chal": b"\xcd" * 50, "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 "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 "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 "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 }, ], ) def test_hmac_vectors(session, vector): key1_id, key2_id, key3_id, key4_id = random.sample(range(1, 0xFFFE), 4) caps = CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC key1 = HmacKey.put( session, key1_id, "Test HMAC Vectors 0x%04x" % key1_id, 1, caps, vector["key"], ALGORITHM.HMAC_SHA1, ) key2 = HmacKey.put( session, key2_id, "Test HMAC Vectors 0x%04x" % key2_id, 1, caps, vector["key"], ALGORITHM.HMAC_SHA256, ) key3 = HmacKey.put( session, key3_id, "Test HMAC Vectors 0x%04x" % key3_id, 1, caps, vector["key"], ALGORITHM.HMAC_SHA384, ) key4 = HmacKey.put( session, key4_id, "Test HMAC Vectors 0x%04x" % key4_id, 1, caps, vector["key"], ALGORITHM.HMAC_SHA512, ) assert key1.sign_hmac(vector["chal"]) == vector["exp_sha1"] assert key2.sign_hmac(vector["chal"]) == vector["exp_sha256"] assert key3.sign_hmac(vector["chal"]) == vector["exp_sha384"] assert key4.sign_hmac(vector["chal"]) == vector["exp_sha512"] assert key1.verify_hmac(vector["exp_sha1"], vector["chal"]) assert key2.verify_hmac(vector["exp_sha256"], vector["chal"]) assert key3.verify_hmac(vector["exp_sha384"], vector["chal"]) assert key4.verify_hmac(vector["exp_sha512"], vector["chal"]) key1.delete() key2.delete() key3.delete() key4.delete() @pytest.mark.parametrize( "vector", [ { "key": b"\x0b" * 65, # Larger than SHA1 block size (64) "chal": b"\xdd" * 50, "algorithm": ALGORITHM.HMAC_SHA1, "exp_sha": b"= 2.3.1 # This ensures test independence across different YubiHSM versions if info.version >= (2, 3, 1): ignored_cmds = [ COMMAND.ERROR, COMMAND.DEVICE_INFO, COMMAND.GET_LOG_ENTRIES, COMMAND.ECHO, COMMAND.SESSION_MESSAGE, ] if info.version < (2, 4, 0): ignored_cmds.extend( [ COMMAND.PUT_PUBLIC_WRAP_KEY, COMMAND.WRAP_KEY_RSA, COMMAND.UNWRAP_KEY_RSA, COMMAND.EXPORT_WRAPPED_RSA, COMMAND.IMPORT_WRAPPED_RSA, ] ) session.set_command_audit( {cmd: AUDIT.ON for cmd in COMMAND if cmd not in ignored_cmds} ) def test_get_log_entries(session): boot, auth, logs = session.get_log_entries() last_digest = logs[0].digest for i in range(1, len(logs)): digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest.update(logs[i].data) digest.update(last_digest) last_digest = digest.finalize()[:16] assert last_digest == logs[i].digest def test_full_log(session): hmackey = HmacKey.generate( session, 0, "Test Full Log", 1, CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC, ALGORITHM.HMAC_SHA256, ) for i in range(0, 30): data = os.urandom(64) resp = hmackey.sign_hmac(data) assert len(resp) == 32 assert hmackey.verify_hmac(resp, data) boot, auth, logs = session.get_log_entries() last_digest = logs[0].digest for i in range(1, len(logs)): digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest.update(logs[i].data) digest.update(last_digest) last_digest = digest.finalize()[:16] assert last_digest == logs[i].digest hmackey.delete() def test_wrong_chain(session): hmackey = HmacKey.generate( session, 0, "Test Log hash chain", 1, CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC, ALGORITHM.HMAC_SHA256, ) boot, auth, logs = session.get_log_entries() last_line = logs.pop() session.set_log_index(last_line.number) hmackey.sign_hmac(b"hello") hmackey.sign_hmac(b"hello") hmackey.sign_hmac(b"hello") with pytest.raises(ValueError): session.get_log_entries(logs.pop()) # Wrong number wrong_line = replace(last_line, digest=os.urandom(16)) with pytest.raises(YubiHsmInvalidResponseError): session.get_log_entries(wrong_line) hmackey.delete() def test_forced_log(hsm, session): boot, auth, logs = session.get_log_entries() last_line = logs.pop() session.set_log_index(last_line.number) session.set_force_audit(AUDIT.ON) assert session.get_force_audit() == AUDIT.ON hmackey = HmacKey.generate( session, 0, "Test Force Log", 1, CAPABILITY.SIGN_HMAC | CAPABILITY.VERIFY_HMAC, ALGORITHM.HMAC_SHA256, ) error = 0 for i in range(0, 32): try: data = os.urandom(64) resp = hmackey.sign_hmac(data) assert len(resp) == 32 assert hmackey.verify_hmac(resp, data) except YubiHsmDeviceError as e: error = e.code assert error == ERROR.LOG_FULL device_info = hsm.get_device_info() assert device_info.log_used == device_info.log_size boot, auth, logs = session.get_log_entries(last_line) last_line = logs.pop() session.set_log_index(last_line.number) session.set_force_audit(AUDIT.OFF) assert session.get_force_audit() == AUDIT.OFF for i in range(0, 32): data = os.urandom(64) resp = hmackey.sign_hmac(data) assert len(resp) == 32 assert hmackey.verify_hmac(resp, data) hmackey.delete() def test_logs_after_reset(hsm, connect_hsm, session, info): session.reset_device() hsm.close() time.sleep(5) # Wait for device to reboot with connect_hsm() as hsm: with hsm.create_session_derived(1, DEFAULT_KEY) as session: boot, auth, logs = session.get_log_entries() # Check the version of the YubiHSM and verify the number of log entries. # YubiHSM versions >= 2.3.1 have command audit logging disabled per default, # so they will only have two initial logs: the boot and reset line. # Versions below 2.3.1 are expected to have four log entries: the boot line, # reset line, create_session and authenticate_session cmd. if info.version < (2, 3, 1): assert 4 == len(logs) else: assert 2 == len(logs) # Reset line assert logs.pop(0).data == b"\0\1" + b"\xff" * 14 # Boot line assert logs.pop(0).data == struct.pack("!HBHHHHBL", 2, 0, 0, 0xFFFF, 0, 0, 0, 0) if info.version < (2, 3, 1): assert logs.pop(0).command == COMMAND.CREATE_SESSION assert logs.pop(0).command == COMMAND.AUTHENTICATE_SESSION yubihsm-3.1.1/tests/device/test_opaque.py0000644000000000000000000000704100000000000015371 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import CAPABILITY, ALGORITHM from yubihsm.objects import Opaque from yubihsm.exceptions import YubiHsmInvalidRequestError from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec import os import uuid import datetime import pytest def get_test_cert(): private_key = ec.generate_private_key( ALGORITHM.EC_P256.to_curve(), default_backend() ) name = x509.Name( [x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "Test Certificate")] ) one_day = datetime.timedelta(1, 0, 0) certificate = ( x509.CertificateBuilder() .subject_name(name) .issuer_name(name) .not_valid_before(datetime.datetime.today() - one_day) .not_valid_after(datetime.datetime.today() + one_day) .serial_number(int(uuid.uuid4())) .public_key(private_key.public_key()) .sign(private_key, hashes.SHA256(), default_backend()) ) return certificate def test_put_empty(session): # Can't put an empty object with pytest.raises(ValueError): Opaque.put( session, 0, "Test PUT empty Opaque", 1, CAPABILITY.NONE, ALGORITHM.OPAQUE_DATA, b"", ) def test_data(session): for size in (1, 256, 1234, 1968): data = os.urandom(size) opaque = Opaque.put( session, 0, "Test data Opaque", 1, CAPABILITY.NONE, ALGORITHM.OPAQUE_DATA, data, ) assert data == opaque.get() opaque.delete() def test_put_too_big(session): with pytest.raises(YubiHsmInvalidRequestError): Opaque.put( session, 0, "Test large Opaque", 1, CAPABILITY.NONE, ALGORITHM.OPAQUE_DATA, os.urandom(3064), ) # Make sure our session is still working assert len(session.get_pseudo_random(123)) == 123 def test_certificate(session): certificate = get_test_cert() certobj = Opaque.put_certificate( session, 0, "Test certificate Opaque", 1, CAPABILITY.NONE, certificate ) assert certificate == certobj.get_certificate() certobj.delete() def test_compressed_certificate(session): certificate = get_test_cert() certobj = Opaque.put_certificate( session, 0, "Test certificate Opaque", 1, CAPABILITY.NONE, certificate, ) compressed_certobj = Opaque.put_certificate( session, 0, "Test certificate Opaque Compressed", 1, CAPABILITY.NONE, certificate, compress=True, ) assert certobj.get_certificate() == compressed_certobj.get_certificate() assert certobj.get() != compressed_certobj.get() assert len(certobj.get()) > len(compressed_certobj.get()) yubihsm-3.1.1/tests/device/test_otp.py0000644000000000000000000001565700000000000014715 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY, ERROR from yubihsm.objects import OtpAeadKey, OtpData from yubihsm.exceptions import YubiHsmDeviceError from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers.aead import AESCCM from cryptography.hazmat.backends import default_backend from dataclasses import dataclass import os import struct import pytest from typing import Mapping @dataclass class OtpTestVector: key: bytes id: bytes otps: Mapping[str, OtpData] # From: https://developers.yubico.com/OTP/Specifications/Test_vectors.html TEST_VECTORS = [ OtpTestVector( key=b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", id=b"\x01\x02\x03\x04\x05\x06", otps={ "dvgtiblfkbgturecfllberrvkinnctnn": OtpData(1, 1, 1, 1), "rnibcnfhdninbrdebccrndfhjgnhftee": OtpData(1, 2, 1, 1), "iikkijbdknrrdhfdrjltvgrbkkjblcbh": OtpData(0xFFF, 1, 1, 1), }, ), OtpTestVector( key=b"\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88", id=b"\x88\x88\x88\x88\x88\x88", otps={"dcihgvrhjeucvrinhdfddbjhfjftjdei": OtpData(0x8888, 0x88, 0x88, 0x8888)}, ), OtpTestVector( key=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", id=b"\x00\x00\x00\x00\x00\x00", otps={"kkkncjnvcnenkjvjgncjihljiibgbhbh": OtpData(0, 0, 0, 0)}, ), OtpTestVector( key=b"\xc4\x42\x28\x90\x65\x30\x76\xcd\xe7\x3d\x44\x9b\x19\x1b\x41\x6a", id=b"\x33\xc6\x9e\x7f\x24\x9e", otps={"iucvrkjiegbhidrcicvlgrcgkgurhjnj": OtpData(1, 0, 0x24, 0x13A7)}, ), ] def _crc16(data): crc = 0xFFFF for b in bytearray(data): crc ^= b for _ in range(8): j = crc & 1 crc >>= 1 if j: crc ^= 0x8408 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 _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 def get_backend(): return MagicMock(YhsmBackend) def simple_urandom(length): """See https://xkcd.com/221/""" return b"\x00" * length @patch("os.urandom", side_effect=simple_urandom) def get_mocked_session(patch): """ Create a fake session by mocking the backend.transceive function """ mocked_backend = get_backend() mocked_backend.transceive.side_effect = [ _TRANSCEIVE_DEVICE_INFO, # get_device_info is called during initialization b"\x83\x00\x11\x00\x05MV1\xc9\x18o\x802%\xed\x8a2$\xf2\xcf", b"\x84\x00\x00", ] hsm = YubiHsm(backend=mocked_backend) auth_key_id = 1 key_enc = b"\t\x0bG\xdb\xedYVT\x90\x1d\xee\x1c\xc6U\xe4 " key_mac = b"Y/\xd4\x83\xf7Y\xe2\x99\t\xa0LE\x05\xd2\xce\n" return hsm.create_session(auth_key_id, key_enc, key_mac) class TestDeviceInfo(unittest.TestCase): """Test the functionality of the DeviceInfo class""" def test_class(self): """ Full testing of the Device Info class """ info = DeviceInfo.parse(_DEVICE_INFO) self.assertEqual(info.version, (2, 0, 0)) self.assertEqual(info.serial, 7550317) self.assertEqual(info.log_size, 62) self.assertEqual(info.log_used, 62) self.assertEqual(len(info.supported_algorithms), 47) class TestDeriveFct(unittest.TestCase): """ Test the functionality of the private method _derive. If we decide not to test private functions, this can be removed. """ def test_success(self): """ Make sure the function works on a test case that should succeed """ context = b"\x0c\xf4\xf5L\xb9\xdfY[" t = 0x04 key = b"0xff0xff0x120xff0xff0xff0xff0xaf" rval = _derive(key, t, context) self.assertEqual(rval, b"W\xa0\x7f1\xb9\x13\xbc\xe5\xff\x066J\x0e9Fz") def test_value_error(self): """ Make sure the test fails when an unsupported value """ context = b"\x0c\xf4\xf5L\xb9\xdfY[" t = 0x04 key = b"0xff0xff0x120xff0xff0xff0xff0xaf" self.assertRaises(ValueError, _derive, key, t, context, 0x60) class TestUnpad(unittest.TestCase): """ Test the functionality of the private method _unpad. If we decide not to test private functions, this can be removed. """ def test_invalid_len(self): """Check if the response length is invalid""" resp = b"\x02\x7f" cmd = COMMAND.SIGN_ECDSA self.assertRaises(YubiHsmInvalidResponseError, _unpad_resp, resp, cmd) resp = b"\x14\x00\x06\x00|\x00\xff" self.assertRaises(YubiHsmInvalidResponseError, _unpad_resp, resp, cmd) def test_device_error(self): """Check if the length of the response doesn't match promised length""" cmd = COMMAND.ERROR resp = b"\x7f\x00\x01\x00\x01\x00>" self.assertRaises(YubiHsmDeviceError, _unpad_resp, resp, cmd) def test_invalid_rcommand(self): """Throw error if the response command doesn't match the command sent | 0x80""" cmd = COMMAND.AUTHENTICATE_SESSION resp = struct.pack("!BHHH", cmd - 1, 1, 1, 62) self.assertRaises(YubiHsmInvalidResponseError, _unpad_resp, resp, cmd) def test_success(self): """Otherwise, succeed""" cmd = COMMAND.AUTHENTICATE_SESSION resp = b"\x84\x00\x02\x00\x01\x00\x1d\x00\x04" rval = _unpad_resp(resp, cmd) self.assertEqual(rval, b"\x00\x01") class TestLogEntry(unittest.TestCase): """ Full testing of the LogEntry class """ def test_construction(self): """Use classmethod `parse` to construct log entry from data""" # Decide on values vals = 513, 250, 1020, 56, 800, 900, 20, 1023, b"abcdefghiklmnop0" keys = ( "number", "command", "length", "session_key", "target_key", "second_key", "result", "tick", "digest", ) # Pack it up for parsing data = struct.pack("!HBHHHHBL16s", *vals) # Use the `parse` alternate constructor log = LogEntry.parse(data) for key, val in zip(keys, vals): self.assertEqual(getattr(log, key), val) # Make sure __init__ and parse give you the same result self.assertEqual(LogEntry(**dict(zip(keys, vals))), log) class TestLogCorrect(unittest.TestCase): """ Full coverage tests of the log class. Includes construction, checks for errors, and validation """ FORMAT = "!HBHHHHBL16s" # The first 2 entries in the log are provided below, along with a # version of log 2 with a tampered hash log1_vals = ( 2, 0, 0, 65535, 0, 0, 0, 0, b"\xf6\x96\x90n[9)\xc6<\xa6\xf1\n\x83\xd2\xa0\xcc", ) log2_vals = ( 3, 3, 10, 65535, 1, 65535, 131, 35, b'"d\xd4Q\xb5\xef\xf5\xdf\xa9LTO3\xb7\x87\xa9', ) # Log 2 is valid, aside from its hash, which doesn't match log2_badvals = ( 3, 3, 10, 65535, 1, 65535, 131, 35, b'"d\xd4Q\xb5\xef\xf5\xdf\xa9LTO3\xb7\x87\xa8', ) log1 = struct.pack(FORMAT, *log1_vals) log2 = struct.pack(FORMAT, *log2_vals) log2_bad = struct.pack(FORMAT, *log2_badvals) e1 = LogEntry.parse(log1) e2 = LogEntry.parse(log2) e2_bad = LogEntry.parse(log2_bad) def test_logvalidation(self): """Make sure we can validate correct, sequential entries""" self.assertTrue(self.e2.validate(self.e1)) def test_unorderedloginvalid(self): """Entries can't validate if out of order""" self.assertRaises(ValueError, self.e1.validate, self.e2) def test_tamperedhash(self): """Bad hashes can be detected""" self.assertFalse(self.e2_bad.validate(self.e1)) def test_data(self): """Check that the data property works""" self.assertTrue(self.e1.data == self.log1[:-16]) def test_initializer(self): """ Check that using the default initializer gives the same result as using the pack constructor """ self.assertTrue(self.e1 == LogEntry(*self.log1_vals)) class TestYubiHsm(unittest.TestCase): @patch("yubihsm.core.YubiHsm.create_session") def test_create_session_derived(self, item): """ Test if create_session_derived calls create_session correctly """ auth_key_id = 1 password = "password" expect_enc = b"\t\x0bG\xdb\xedYVT\x90\x1d\xee\x1c\xc6U\xe4 " expect_mac = b"Y/\xd4\x83\xf7Y\xe2\x99\t\xa0LE\x05\xd2\xce\n" # Note: get_device_info gets called during initialization # which is why we mock the transceive function. backend = get_backend() backend.transceive.return_value = _TRANSCEIVE_DEVICE_INFO hsm = YubiHsm(backend) hsm.create_session_derived(auth_key_id, password) hsm.create_session.assert_called_once_with(auth_key_id, expect_enc, expect_mac) def test_get_device_info_mock_transceive(self): """ Test get_device_info function by mocking the transceive function """ backend = get_backend() backend.transceive.return_value = _TRANSCEIVE_DEVICE_INFO hsm = YubiHsm(backend) info = hsm.get_device_info() hsm._backend.transceive.assert_has_calls( [ call(b"\x06\x00\x00"), # first call during YubiHSM::__init__ call(b"\x06\x00\x00"), ] ) self.assertEqual(info.version, (2, 0, 0)) self.assertEqual(info.serial, 7550140) self.assertEqual(info.log_size, 62) self.assertEqual(info.log_used, 4) self.assertEqual(len(info.supported_algorithms), 47) class TestAuthsession(unittest.TestCase): def test_list_objects1(self): """ Test the first half of the list_objects function: We process the input and make our query to the send_cmd """ # Create fake session, and mock the return from a call to side_effect session = MagicMock(AuthSession) session.send_secure_cmd.side_effect = [b"\x00\x01\x02\x00V7\x03\x00"] # Run the function, and make sure the correct call is made to secure_cmd AuthSession.list_objects( session, object_id=2, object_type=1, domains=65535, capabilities=CAPABILITY.ALL, algorithm=ALGORITHM.HMAC_SHA384, ) # We care only about the value sent; we aren't checking the return value # from send_secure_cmd session.send_secure_cmd.assert_called_with( 72, b"\x01\x00\x02\x02\x01\x03\xff\xff\x04\x00\xff\xff\xff\xff\xff\xff\xff\x05\x15", # noqa E501 ) def test_list_objects2(self): """ Test the second half of list_objects(): process the response from the send_cmd function and return the list of objects """ list_objects = AuthSession.list_objects session = MagicMock(AuthSession) 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 # The input to the below function doesn't matter; # it's overwritten by the return value listed above objlist = list_objects(session) # Finally, make sure we decode the results correctly # Not the best way to check, but it is succinct self.assertEqual( objlist.__repr__(), "[AuthenticationKey(id=1), AsymmetricKey(id=25743), AsymmetricKey(id=1), HmacKey(id=4), HmacKey(id=5), AsymmetricKey(id=5)]", # noqa E501 ) def test__create_session_patch_transceive(self): """ Tests the entire authsession generation codebase, by mocking just os.urandom and the transceive method. This test should probably be broken up later. """ authsession = get_mocked_session() # Create session should make two calls to transceive. # First call was to create session. Second was to authenticate session. calls = [ call(b"\x03\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"), call(b"\x04\x00\x11\x00\x1c\xe7U.\x0fyv\xdb\xcc!\x98\xfd\x15\\3Z"), ] authsession._hsm._backend.transceive.assert_has_calls(calls) def test_close_session(self): """ Test the close session command; only tests output to send_secure_cmd not to transceive """ session = get_mocked_session() session.send_secure_cmd = MagicMock(session.send_secure_cmd) session.close() session.send_secure_cmd.assert_called_once_with(COMMAND.CLOSE_SESSION) def test_reset(self): """ Tests the reset command; only tests output to send_secure_cmd not to transceive """ session = get_mocked_session() session.send_secure_cmd = MagicMock(session.send_secure_cmd) session.send_secure_cmd.return_value = b"" session.reset_device() session.send_secure_cmd.assert_called_with(COMMAND.RESET_DEVICE) def test_reset_error(self): """ Make sure reset command throws error if nonempty response is returned """ session = get_mocked_session() session.send_secure_cmd = MagicMock(session.send_secure_cmd) session.send_secure_cmd.return_value = b"\00" self.assertRaises(YubiHsmInvalidResponseError, session.reset_device) yubihsm-3.1.1/tests/test_defs.py0000644000000000000000000000275100000000000013564 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.defs import ALGORITHM, CAPABILITY from cryptography.hazmat.primitives.asymmetric import ec import pytest @pytest.mark.parametrize( "algorithm, curve", [ (ALGORITHM.EC_P224, ec.SECP224R1), (ALGORITHM.EC_P256, ec.SECP256R1), (ALGORITHM.EC_P384, ec.SECP384R1), (ALGORITHM.EC_P521, ec.SECP521R1), (ALGORITHM.EC_K256, ec.SECP256K1), (ALGORITHM.EC_BP256, ec.BrainpoolP256R1), (ALGORITHM.EC_BP384, ec.BrainpoolP384R1), (ALGORITHM.EC_BP512, ec.BrainpoolP512R1), ], ) def test_algorithm_to_from_curve(algorithm, curve): assert isinstance(algorithm.to_curve(), curve) assert algorithm == ALGORITHM.for_curve(curve()) def test_capability_all_includes_everything(): assert CAPABILITY.ALL == sum(CAPABILITY) assert CAPABILITY.NONE == 0 for c in CAPABILITY: assert c in CAPABILITY.ALL assert c not in CAPABILITY.NONE yubihsm-3.1.1/tests/test_objects.py0000644000000000000000000001040500000000000014267 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.objects import ( ObjectInfo, YhsmObject, Opaque, AuthenticationKey, AsymmetricKey, WrapKey, PublicWrapKey, HmacKey, Template, OtpAeadKey, ) from yubihsm.core import AuthSession from yubihsm.defs import ORIGIN, ALGORITHM, OBJECT, CAPABILITY from binascii import a2b_hex from unittest.mock import MagicMock from random import randint import unittest _DATA = a2b_hex( "ffffffffffffffff00010028ffff0226000244454641554c5420415554484b4559204348414e47452054484953204153415000c0ffeec0ffee01ffffffffffffffff" # noqa E501 ) class TestObjectInfo(unittest.TestCase): def test_objectinfo_parsing(self): info = ObjectInfo.parse(_DATA) self.assertEqual(info.capabilities, 0xFFFFFFFFFFFFFFFF) self.assertEqual(info.id, 1) self.assertEqual(info.size, 40) self.assertEqual(info.domains, 0xFFFF) self.assertEqual(info.object_type, OBJECT.AUTHENTICATION_KEY) self.assertEqual(info.algorithm, ALGORITHM.AES128_YUBICO_AUTHENTICATION) self.assertEqual(info.sequence, 0) self.assertEqual(info.origin, ORIGIN.IMPORTED) self.assertEqual(info.label, "DEFAULT AUTHKEY CHANGE THIS ASAP") self.assertEqual(info.delegated_capabilities, 0xFFFFFFFFFFFFFFFF) def test_non_utf8_label(self): label = b"\xfe\xed\xfa\xce" * 10 data = bytearray(_DATA) data[18:58] = label info = ObjectInfo.parse(bytes(data)) self.assertEqual(info.label, label) self.assertIsInstance(info.label, bytes) class TestYhsmObject(unittest.TestCase): def test_get_info(self): AuthMock = MagicMock(AuthSession) 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 # Create instance from mocked data and set the object type obj = YhsmObject(session=AuthMock, object_id=5) obj.object_type = OBJECT.HMAC_KEY # The expected ObjectInfo is below info = ObjectInfo( capabilities=CAPABILITY(140737488355327), id=5, size=256, domains=41, object_type=OBJECT.HMAC_KEY, algorithm=ALGORITHM.HMAC_SHA512, sequence=0, origin=ORIGIN.GENERATED, label="hmaclabel", delegated_capabilities=0, ) self.assertEqual(info, obj.get_info()) def test_delete(self): AuthMock = MagicMock(AuthSession) AuthMock.send_secure_cmd.return_value = b"" obj = YhsmObject(session=AuthMock, object_id=5) obj.object_type = OBJECT.HMAC_KEY obj.delete() def test__create(self): # create for every type items = [ (OBJECT.OPAQUE, Opaque), (OBJECT.AUTHENTICATION_KEY, AuthenticationKey), (OBJECT.ASYMMETRIC_KEY, AsymmetricKey), (OBJECT.WRAP_KEY, WrapKey), (OBJECT.PUBLIC_WRAP_KEY, PublicWrapKey), (OBJECT.HMAC_KEY, HmacKey), (OBJECT.TEMPLATE, Template), (OBJECT.OTP_AEAD_KEY, OtpAeadKey), ] AuthMock = MagicMock(AuthSession) for obj_type, obj_class in items: id_num = randint(1, 17) obj = YhsmObject._create(obj_type, AuthMock, id_num) self.assertIsInstance(obj, obj_class) self.assertEqual(obj.id, id_num) expected_repr = "{class_name}(id={id_num})".format( class_name=obj_class.__name__, id_num=id_num ) self.assertEqual(obj.__repr__(), expected_repr) yubihsm-3.1.1/tests/test_utils.py0000644000000000000000000000311000000000000013771 0ustar00# coding=utf-8 # Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from yubihsm.utils import password_to_key from binascii import a2b_hex import unittest class TestUtils(unittest.TestCase): def test_password_to_key(self): self.assertEqual( ( a2b_hex("090b47dbed595654901dee1cc655e420"), a2b_hex("592fd483f759e29909a04c4505d2ce0a"), ), password_to_key("password"), ) def test_password_to_key_utf8(self): self.assertEqual( ( a2b_hex("f320972c667ba5cd4d35119a6b0271a1"), a2b_hex("f10050ca688e5a6ce62b1ffb0f6f6869"), ), password_to_key("κόσμε"), ) def test_password_to_key_bytes_fails(self): with self.assertRaises(AttributeError): self.assertRaises(AttributeError, password_to_key(b"password")) with self.assertRaises(AttributeError): self.assertRaises( AttributeError, password_to_key(a2b_hex("cebae1bdb9cf83cebcceb5")) ) yubihsm-3.1.1/yubihsm/__init__.py0000644000000000000000000000161500000000000013657 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Contains the main YubiHsm class used to connect to a YubiHSM device. See :class:`~yubihsm.core.YubiHsm`. :Example: >>> from yubihsm import YubiHsm ... hsm = YubiHsm.connect('http://localhost:12345') ... session = hsm.create_session_derived(1, 'password') """ from .core import YubiHsm # noqa F401 __version__ = "3.1.1" yubihsm-3.1.1/yubihsm/backends/__init__.py0000644000000000000000000000360100000000000015426 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import re from typing import Optional from urllib import parse class YhsmBackend(abc.ABC): """Provides low-level communication with a YubiHSM.""" @abc.abstractmethod def transceive(self, msg: bytes) -> bytes: """Send a verbatim message.""" @abc.abstractmethod def close(self) -> None: """Closes the connection to the YubiHSM.""" def get_backend(url: Optional[str] = None) -> YhsmBackend: """Returns a backend suitable for the given URL.""" url = url or "http://localhost:12345" parsed = parse.urlparse(url) try: if parsed.scheme == "yhusb": from .usb import UsbBackend serial = re.match(r"serial=(\d+)", parsed.netloc) if serial: return UsbBackend(serial=int(serial.group(1)), timeout=600) elif not parsed.netloc: # On anything else, fall through to error. return UsbBackend(serial=None, timeout=600) elif parsed.scheme in ("http", "https"): from .http import HttpBackend return HttpBackend(url, (10, 600)) except ImportError: raise ValueError( 'Unable to initialize backend for scheme "%s", are ' "required dependencies installed?" % parsed.scheme ) raise ValueError("Invalid YubiHSM backend URL.") yubihsm-3.1.1/yubihsm/backends/http.py0000644000000000000000000000433000000000000014646 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Optional, Tuple, Union from urllib import parse import requests from requests.exceptions import RequestException from ..exceptions import YubiHsmConnectionError from . import YhsmBackend class HttpBackend(YhsmBackend): """A backend for communicating with a YubiHSM connector over HTTP.""" def __init__( self, url: str = "http://localhost:12345", timeout: Optional[Union[Tuple[int, int], int]] = None, ): """Construct a new HttpBackend, connecting to the given URL. The URL should be a http(s) URL to a running YubiHSM connector. By default, the backend will attempt to connect to a connector running locally, on the default port. :param str url: (optional) The URL to connect to. :param timeout: (optional) A timeout in seconds, or a tuple of two values to use as connection timeout and request timeout. :type timeout: int or tuple[int, int] """ if not url.endswith("/"): url = url + "/" self._url = parse.urljoin(url, "connector/api") self._timeout = timeout self._session = requests.Session() self._session.headers.update({"Content-Type": "application/octet-stream"}) def transceive(self, msg): try: resp = self._session.post(url=self._url, data=msg, timeout=self._timeout) resp.raise_for_status() return resp.content except RequestException as e: raise YubiHsmConnectionError(e) def close(self): self._session.close() def __repr__(self): return '{0.__class__.__name__}("{0._url}")'.format(self) yubihsm-3.1.1/yubihsm/backends/usb.py0000644000000000000000000000645700000000000014474 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Optional import usb.core import usb.util from ..exceptions import YubiHsmConnectionError from . import YhsmBackend YUBIHSM_VID = 0x1050 YUBIHSM_PID = 0x0030 class UsbBackend(YhsmBackend): """A backend for communicating with a YubiHSM directly over USB.""" def __init__(self, serial: Optional[int] = None, timeout: Optional[int] = None): """Construct a UsbBackend, connected to a YubiHSM via USB. :param serial: (optional) The serial number of the YubiHSM to connect to. :param timeout: (optional) A read/write timeout in seconds. """ err = None for device in usb.core.find( find_all=True, idVendor=YUBIHSM_VID, idProduct=YUBIHSM_PID ): # type: ignore try: cfg = device.get_active_configuration() # type: ignore except usb.core.USBError: cfg = None if cfg is None or cfg.bConfigurationValue != 0x01: # type: ignore try: device.set_configuration(0x01) # type: ignore except usb.core.USBError as e: err = YubiHsmConnectionError(e) continue if serial is None or int(device.serial_number) == serial: # type: ignore break usb.util.dispose_resources(device) else: raise err or YubiHsmConnectionError("No YubiHSM found.") # Flush any data waiting to be read try: device.read(0x81, 0xFFFF, 10) # type: ignore except usb.core.USBError: pass # Errors here are expected, and ignored self._device = device # pyusb expects milliseconds or None if no timeout self.timeout = None if timeout is None else timeout * 1000 def transceive(self, msg): try: sent = self._device.write(0x01, msg, self.timeout) # type: ignore if sent != len(msg): raise YubiHsmConnectionError("Error sending data over USB.") if sent % 64 == 0: if self._device.write(0x01, b"", self.timeout) != 0: # type: ignore raise YubiHsmConnectionError("Error sending data over USB.") return bytes(bytearray(self._device.read(0x81, 0xFFFF, self.timeout))) # type: ignore except usb.core.USBError as e: raise YubiHsmConnectionError(e) def close(self): usb.util.dispose_resources(self._device) def __repr__(self): v_int = self._device.bcdDevice # type: ignore version = "{}.{}.{}".format((v_int >> 8) & 0xF, (v_int >> 4) & 0xF, v_int & 0xF) return ( "{0.__class__.__name__}(version={1}, serial={0._device.serial_number})" ).format(self, version) yubihsm-3.1.1/yubihsm/core.py0000644000000000000000000010472000000000000013051 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Core classes for YubiHSM communication.""" import os import struct import warnings from dataclasses import astuple, dataclass from hashlib import sha256 from typing import ClassVar, Mapping, NamedTuple, Optional, Sequence, Set, Tuple from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import cmac, constant_time, hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF from . import utils from .backends import YhsmBackend, get_backend from .defs import ( ALGORITHM, AUDIT, COMMAND, ERROR, FIPS_STATUS, LIST_FILTER, OBJECT, OPTION, Version, ) from .exceptions import ( YubiHsmAuthenticationError, YubiHsmConnectionError, YubiHsmDeviceError, YubiHsmInvalidRequestError, YubiHsmInvalidResponseError, ) from .objects import LABEL_LENGTH, YhsmObject, _label_pack KEY_ENC = 0x04 KEY_MAC = 0x06 KEY_RMAC = 0x07 CARD_CRYPTOGRAM = 0x00 HOST_CRYPTOGRAM = 0x01 def _derive(key: bytes, t: int, context: bytes, L: int = 0x80) -> bytes: # this only supports aes128 if L != 0x80 and L != 0x40: raise ValueError("L must be 0x40 or 0x80") i = b"\0" * 11 + struct.pack("!BBHB", t, 0, L, 1) + context c = cmac.CMAC(algorithms.AES(key), backend=default_backend()) c.update(i) return c.finalize()[: L // 8] def _unpad_resp(resp: bytes, cmd: COMMAND) -> bytes: if len(resp) < 3: raise YubiHsmInvalidResponseError("Wrong length") rcmd, length = struct.unpack("!BH", resp[:3]) if len(resp) < length + 3: raise YubiHsmInvalidResponseError("Wrong length") if rcmd == COMMAND.ERROR: raise YubiHsmDeviceError(resp[3]) elif rcmd != cmd | 0x80: raise YubiHsmInvalidResponseError("Wrong command in response") return resp[3 : length + 3] class _UnknownIntEnum(int): name = "UNKNOWN" def __repr__(self): return "<%s: %d>" % (self.name, self) def __str__(self): return self.name @property def value(self) -> int: return int(self) class _UnknownAlgorithm(_UnknownIntEnum): """Wrapper for unknown ALGORITHM values. Provides obj.name, obj.value and and string representations.""" name = "ALGORITHM.UNKNOWN" def _algorithm(val: int) -> ALGORITHM: try: return ALGORITHM(val) except ValueError: return _UnknownAlgorithm(val) # type: ignore class _UnknownCommand(_UnknownIntEnum): """Wrapper for unknown COMMAND values. Provides obj.name, obj.value and and string representations.""" name = "COMMAND.UNKNOWN" @dataclass(frozen=True) class DeviceInfo: """Data class holding various information about the YubiHSM. :ivar version: YubiHSM version tuple. :ivar serial: YubiHSM serial number. :ivar log_size: Log entry storage capacity. :ivar log_used: Log entries currently stored. :ivar supported_algorithms: List of supported algorithms. :ivar part_number: Chip designator. """ FORMAT: ClassVar[str] = "!BBBIBB" LENGTH: ClassVar[int] = struct.calcsize(FORMAT) version: Version serial: int log_size: int log_used: int supported_algorithms: Set[ALGORITHM] part_number: Optional[str] @classmethod def parse( cls, first_page: bytes, second_page: Optional[bytes] = None ) -> "DeviceInfo": """Parse a DeviceInfo from its binary representation.""" unpacked = struct.unpack_from(cls.FORMAT, first_page) version: Version = unpacked[:3] # type: ignore serial, log_size, log_used = unpacked[3:] algorithms = {_algorithm(a) for a in first_page[cls.LENGTH :]} part_number = None if second_page: part_number = second_page.decode("utf-8") return cls(version, serial, log_size, log_used, algorithms, part_number) def _calculate_iv(key: bytes, counter: int) -> bytes: encryptor = Cipher( algorithms.AES(key), modes.ECB(), # noqa: S305 backend=default_backend(), ).encryptor() return encryptor.update(int.to_bytes(counter, 16, "big")) + encryptor.finalize() def _calculate_mac(key: bytes, chain: bytes, message: bytes) -> Tuple[bytes, bytes]: c = cmac.CMAC(algorithms.AES(key), backend=default_backend()) c.update(chain) c.update(message) chain = c.finalize() return chain, chain[:8] @dataclass(frozen=True) class LogEntry: """YubiHSM log entry. :param int number: The sequence number of the entry. :param int command: The COMMAND executed. :param int length: The length of the command. :param int session_key: The ID of the Authentication Key for the session. :param int target_key: The ID of the key used by the command. :param int second_key: The ID of the secondary key used by the command, if applicable. :param int result: The result byte of the response. :param int tick: The YubiHSM system tick value when the command was run. :param bytes digest: A truncated hash of the entry and previous digest. """ FORMAT: ClassVar[str] = "!HBHHHHBL16s" LENGTH: ClassVar[int] = struct.calcsize(FORMAT) number: int command: COMMAND length: int session_key: int target_key: int second_key: int result: int tick: int digest: bytes @property def data(self) -> bytes: """Get log entry binary data. :return: The binary LogEntry data, excluding the digest. """ return struct.pack(self.FORMAT, *astuple(self))[:-16] @classmethod def parse(cls, data: bytes) -> "LogEntry": """Parse a LogEntry from its binary representation. :param data: Binary data to unpack from. :return: The parsed object. """ unpacked = list(struct.unpack(cls.FORMAT, data)) try: unpacked[1] = COMMAND(unpacked[1]) except ValueError: unpacked[1] = _UnknownCommand(unpacked[1]) return cls(*unpacked) def validate(self, previous_entry: "LogEntry") -> bool: """Validate the hash of a single log entry. Validates the hash of this entry with regard to the previous entry's hash. The previous entry is the LogEntry with the previous number, previous_entry.number == self.number - 1 :param previous_entry: The previous log entry to validate against. :return: True if the digest is correct, False if not. """ if (self.number - previous_entry.number) & 0xFFFF != 1: raise ValueError("previous_entry has wrong number!") digest = sha256(self.data + previous_entry.digest).digest()[:16] return constant_time.bytes_eq(self.digest, digest) class LogData(NamedTuple): """Data class holding response data from a GET_LOGS command. :param n_boot: Number of unlogged boot events. :param n_auth: Number of unlogged authentication events. :param entries: List of LogEntry items. """ n_boot: int n_auth: int entries: Sequence[LogEntry] class _ClosedBackend(YhsmBackend): def transceive(self, msg): raise TypeError("The backend has been closed!") def close(self): pass class YubiHsm: """An unauthenticated connection to a YubiHSM.""" def __init__(self, backend: YhsmBackend): """Constructs a YubiHSM connected to the given backend. :param backend: A backend used to communicate with a YubiHSM. """ self._backend: YhsmBackend = backend # Initialize the message buffer size to 2048 bytes. This may be updated # depending on the YubiHSM FW version (in 2.4.0 or higher the # buffer size is 3136) in get_device_info. self._msg_buf_size = 2048 self.get_device_info() def __enter__(self): return self def __exit__(self, typ, value, traceback): self.close() def close(self) -> None: """Disconnect from the backend, freeing any resources in use by it.""" if self._backend: self._backend.close() self._backend = _ClosedBackend() def _transceive(self, msg: bytes) -> bytes: if len(msg) > self._msg_buf_size - 1: raise YubiHsmInvalidRequestError("Message too long.") return self._backend.transceive(msg) def send_cmd(self, cmd: COMMAND, data: bytes = b"") -> bytes: """Encode and send a command byte and its associated data. :param cmd: The command to send. :param data: The command payload to send. :return: The response data from the YubiHSM. """ msg = struct.pack("!BH", cmd, len(data)) + data return _unpad_resp(self._transceive(msg), cmd) def get_device_info(self) -> DeviceInfo: """Get general device information from the YubiHSM. :return: Device information. """ first_page = self.send_cmd(COMMAND.DEVICE_INFO) device_info = DeviceInfo.parse(first_page) if device_info.version >= (2, 4, 0): # Update maximum message buffer size self._msg_buf_size = 3136 # fetch next page second_page = self.send_cmd(COMMAND.DEVICE_INFO, struct.pack("!B", 1)) device_info = DeviceInfo.parse(first_page, second_page) return device_info def get_device_public_key(self) -> ec.EllipticCurvePublicKey: """Retrieve the device's public key. :return: The device public key. """ resp = self.send_cmd(COMMAND.GET_DEVICE_PUBLIC_KEY) algorithm, public_key = resp[0], resp[1:] if algorithm != ALGORITHM.EC_P256_YUBICO_AUTHENTICATION: raise YubiHsmInvalidResponseError() return ec.EllipticCurvePublicKey.from_encoded_point( ec.SECP256R1(), b"\x04" + public_key ) def init_session(self, auth_key_id: int) -> "SymmetricAuth": """Initiate the symmetric authentication process for establishing an authenticated session with the YubiHSM. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :return: A negotiation of an authenticated Session with a YubiHSM. """ return SymmetricAuth.init_session(self, auth_key_id) def init_session_asymmetric( self, auth_key_id: int, epk_oce: bytes ) -> "AsymmetricAuth": """Initiate the asymmetric authentication process for establishing an authenticated session with the YubiHSM. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :param epk_oce: The ephemeral public key of the OCE used for key agreement. """ return AsymmetricAuth.init_session(self, auth_key_id, epk_oce) def create_session( self, auth_key_id: int, key_enc: bytes, key_mac: bytes ) -> "AuthSession": """Create an authenticated session with the YubiHSM. See also create_session_derived, which derives K-ENC and K-MAC from a password. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :param key_enc: Static K-ENC used to establish session. :param key_mac: Static K-MAC used to establish session. :return: An authenticated session. """ return SymmetricAuth.create_session(self, auth_key_id, key_enc, key_mac) def create_session_derived(self, auth_key_id: int, password: str) -> "AuthSession": """Create an authenticated session with the YubiHSM. Uses a supplied password to derive the keys K-ENC and K-MAC. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :param password: The password used to derive the keys from. :return: An authenticated session. """ key_enc, key_mac = utils.password_to_key(password) return self.create_session(auth_key_id, key_enc, key_mac) def create_session_asymmetric( self, auth_key_id: int, private_key: ec.EllipticCurvePrivateKey, public_key: Optional[ec.EllipticCurvePublicKey] = None, ) -> "AuthSession": """Create an authenticated session with the YubiHSM. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :param private_key: Private key corresponding to the public authentication key object. :param public_key: The device's public key. If omitted, the public key is fetched from the YubiHSM. :return: An authenticated session. """ if public_key is None: public_key = self.get_device_public_key() return AsymmetricAuth.create_session(self, auth_key_id, private_key, public_key) @classmethod def connect(cls, url: Optional[str] = None) -> "YubiHsm": """Return a YubiHsm connected to the backend specified by the URL. If no URL is given this will attempt to connect to a YubiHSM connector running on localhost, using the default port. :param url: A http(s):// or yhusb:// backend URL. :return: A YubiHsm instance connected to the backend referenced by the url. """ return cls(get_backend(url)) def __repr__(self): return "{0.__class__.__name__}({0._backend})".format(self) class SymmetricAuth: """A negotiation of an authenticated Session with a YubiHSM. This class is used to begin the mutual authentication process for establishing an authenticated session with the YubiHSM, using symmetric authentication. Typically you get an instance of this class by calling :func:`~YubiHsm.init_session`. """ def __init__(self, hsm: YubiHsm, sid: int, context: bytes, card_crypto: bytes): self._hsm = hsm self._sid = sid self._context = context self._card_crypto = card_crypto @property def context(self) -> bytes: """The authentication context (host challenge + card challenge).""" return self._context @property def card_crypto(self) -> bytes: """The card cryptogram.""" return self._card_crypto @classmethod def init_session( cls, hsm: YubiHsm, auth_key_id: int, ) -> "SymmetricAuth": """Initiate the mutual symmetric session authentication process. :param hsm: The YubiHSM connection. :param auth_key_id: The ID of the Authentication key used to authenticate the session. """ context = os.urandom(8) data = hsm.send_cmd( COMMAND.CREATE_SESSION, struct.pack("!H", auth_key_id) + context ) sid = data[0] context += data[1 : 1 + 8] card_crypto = data[9 : 9 + 8] return cls(hsm, sid, context, card_crypto) @classmethod def create_session( cls, hsm: YubiHsm, auth_key_id: int, key_enc: bytes, key_mac: bytes ) -> "AuthSession": """Construct an authenticated session. :param hsm: The YubiHSM connection. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :param key_enc: Static `K-ENC` used to establish the session. :param key_mac: Static `K-MAC` used to establish the session. """ symmetric_auth = cls.init_session(hsm, auth_key_id) key_senc = _derive(key_enc, KEY_ENC, symmetric_auth.context) key_smac = _derive(key_mac, KEY_MAC, symmetric_auth.context) key_srmac = _derive(key_mac, KEY_RMAC, symmetric_auth.context) return symmetric_auth.authenticate(key_senc, key_smac, key_srmac) def authenticate( self, key_senc: bytes, key_smac: bytes, key_srmac: bytes ) -> "AuthSession": """Construct an authenticated session. :param key_senc: `S-ENC` used for data confidentiality. :param key_smac: `S-MAC` used for data and protocol integrity. :param key_srmac: `S-RMAC` used for data and protocol integrity. :return: An authenticated session. """ gen_card_crypto = _derive(key_smac, CARD_CRYPTOGRAM, self._context, 0x40) if not constant_time.bytes_eq(gen_card_crypto, self._card_crypto): raise YubiHsmAuthenticationError() msg = struct.pack("!BHB", COMMAND.AUTHENTICATE_SESSION, 1 + 8 + 8, self._sid) msg += _derive(key_smac, HOST_CRYPTOGRAM, self._context, 0x40) mac_chain, mac = _calculate_mac(key_smac, b"\0" * 16, msg) msg += mac if _unpad_resp(self._hsm._transceive(msg), COMMAND.AUTHENTICATE_SESSION) != b"": raise YubiHsmInvalidResponseError("Non-empty response") return AuthSession( self._hsm, self._sid, key_senc, key_smac, key_srmac, mac_chain ) class AsymmetricAuth: """A negotiation of an authenticated Session with a YubiHSM. This class is used to begin the mutual authentication process for establishing an authenticated session with the YubiHSM, using asymmetric authentication. Typically you get an instance of this class by calling :func:`~YubiHsm.init_session_asymmetric`. """ def __init__( self, hsm: YubiHsm, sid: int, context: bytes, receipt: bytes, ): self._hsm = hsm self._sid = sid self._context = context self._receipt = receipt @property def context(self) -> bytes: """The authentication context (EPK.OCE + EPK.SD).""" return self._context @property def receipt(self) -> bytes: """The receipt.""" return self._receipt @property def epk_hsm(self) -> bytes: """The ephemeral public key of the YubiHSM.""" return self._context[65:] @classmethod def init_session( cls, hsm: YubiHsm, auth_key_id: int, epk_oce: bytes, ) -> "AsymmetricAuth": """Initiate the mutual asymmetric session authentication process. :param hsm: The YubiHSM connection. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :param epk_oce: The ephemeral public key of the OCE used for key agreement. """ public_key_len = len(epk_oce) msg = struct.pack("!H", auth_key_id) + epk_oce resp = hsm.send_cmd(COMMAND.CREATE_SESSION, msg) sid, epk_hsm, receipt = ( resp[0], resp[1 : 1 + public_key_len], resp[1 + public_key_len :], ) context = epk_oce + epk_hsm return cls(hsm, sid, context, receipt) @classmethod def create_session( cls, hsm: YubiHsm, auth_key_id: int, private_key: ec.EllipticCurvePrivateKey, public_key: ec.EllipticCurvePublicKey, ) -> "AuthSession": """Construct an authenticated session. :param hsm: The YubiHSM connection. :param auth_key_id: The ID of the Authentication key used to authenticate the session. :param private_key: Private key corresponding to the public authentication key object. :param public_key: The device's public key. """ # Calculate shared secret from the two static keys. shsss = private_key.exchange(ec.ECDH(), public_key) # Generate an ephemeral key. esk_oce = ec.generate_private_key(private_key.curve, backend=default_backend()) epk_oce = esk_oce.public_key().public_bytes( encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint, ) # Exchange ephemereal keys with the HSM asymmetric_auth = cls.init_session(hsm, auth_key_id, epk_oce) # Calculate shared secret from the two ephemeral keys. shsee = esk_oce.exchange( ec.ECDH(), ec.EllipticCurvePublicKey.from_encoded_point( private_key.curve, asymmetric_auth.epk_hsm ), ) # Derive session keys. Note that this generates four keys, the # first of which is used to verify the receipt. shs = X963KDF( hashes.SHA256(), 4 * 16, b"\x3c\x88\x10", backend=default_backend() ).derive(shsee + shsss) keys = (shs[i : i + 16] for i in range(0, len(shs), 16)) # Verify the receipt. c = cmac.CMAC(algorithms.AES(next(keys)), backend=default_backend()) c.update(asymmetric_auth.epk_hsm) c.update(epk_oce) if not constant_time.bytes_eq(c.finalize(), asymmetric_auth.receipt): raise YubiHsmAuthenticationError() return asymmetric_auth.authenticate(next(keys), next(keys), next(keys)) def authenticate( self, key_senc: bytes, key_smac: bytes, key_srmac: bytes ) -> "AuthSession": """Construct an authenticated session. :param key_senc: `S-ENC` used for data confidentiality. :param key_smac: `S-MAC` used for data and protocol integrity. :param key_srmac: `S-RMAC` used for data and protocol integrity. :return: An authenticated session. """ return AuthSession( self._hsm, self._sid, key_senc, key_smac, key_srmac, self._receipt ) class AuthSession: """An authenticated secure session with a YubiHSM. Typically you get an instance of this class by calling :func:`~YubiHsm.create_session`, :func:`~YubiHsm.create_session_derived`, or :func:`~YubiHsm.create_session_asymmetric`. """ def __init__( self, hsm: YubiHsm, sid: int, key_enc: bytes, key_mac: bytes, key_rmac: bytes, mac_chain: bytes, ): self._hsm = hsm self._sid: Optional[int] = sid self._key_enc = key_enc self._key_mac = key_mac self._key_rmac = key_rmac self._mac_chain = mac_chain self._ctr = 1 def close(self) -> None: """Close this session with the YubiHSM. Once closed, this session object can no longer be used, unless re-connected. """ if self._sid is not None: try: self.send_secure_cmd(COMMAND.CLOSE_SESSION) finally: self._sid = None self._key_enc = self._key_mac = self._key_rmac = b"" def __enter__(self): return self def __exit__(self, typ, value, traceback): self.close() def _secure_transceive(self, msg: bytes) -> bytes: padlen = 15 - len(msg) % 16 msg += b"\x80" msg = msg.ljust(len(msg) + padlen, b"\0") wrapped = struct.pack( "!BHB", COMMAND.SESSION_MESSAGE, 1 + len(msg) + 8, self.sid ) cipher = Cipher( algorithms.AES(self._key_enc), modes.CBC(_calculate_iv(self._key_enc, self._ctr)), backend=default_backend(), ) encryptor = cipher.encryptor() wrapped += encryptor.update(msg) + encryptor.finalize() next_mac_chain, mac = _calculate_mac(self._key_mac, self._mac_chain, wrapped) wrapped += mac raw_resp = self._hsm._transceive(wrapped) data = _unpad_resp(raw_resp, COMMAND.SESSION_MESSAGE) if data[0] != self._sid: raise YubiHsmInvalidResponseError("Incorrect SID") rmac = _calculate_mac(self._key_rmac, next_mac_chain, raw_resp[:-8])[1] if not constant_time.bytes_eq(raw_resp[-8:], rmac): raise YubiHsmInvalidResponseError("Incorrect MAC") self._ctr += 1 self._mac_chain = next_mac_chain decryptor = cipher.decryptor() return decryptor.update(data[1:-8]) + decryptor.finalize() @property def sid(self) -> Optional[int]: """Session ID :return: The ID of the session. """ return self._sid def send_secure_cmd(self, cmd: COMMAND, data: bytes = b"") -> bytes: """Send a command over the encrypted session. :param cmd: The command to send. :param data: The command payload to send. :return: The decrypted response data from the YubiHSM. """ msg = struct.pack("!BH", cmd, len(data)) + data return _unpad_resp(self._secure_transceive(msg), cmd) def list_objects( self, object_id: Optional[int] = None, object_type: Optional[OBJECT] = None, domains: Optional[int] = None, capabilities: Optional[int] = None, algorithm: Optional[ALGORITHM] = None, label: Optional[str] = None, ) -> Sequence[YhsmObject]: """List objects from the YubiHSM. This returns a list of all objects currently stored on the YubiHSM, which are accessible by this session. The arguments to this method can be used to filter the results returned. :param object_id: Return only objects with this ID. :param object_type: Return only objects of this type. :param domains: Return only objects belonging to one or more of these domains. :param capabilities: Return only objects with one or more of these capabilities. :param algorithm: Return only objects with this algorithm. :param label: Return only objects with this label. :return: A list of matched objects. """ msg = b"" if object_id is not None: msg += struct.pack("!BH", LIST_FILTER.ID, object_id) if object_type is not None: msg += struct.pack("!BB", LIST_FILTER.TYPE, object_type) if domains is not None: msg += struct.pack("!BH", LIST_FILTER.DOMAINS, domains) if capabilities is not None: msg += struct.pack("!BQ", LIST_FILTER.CAPABILITIES, capabilities) if algorithm is not None: msg += struct.pack("!BB", LIST_FILTER.ALGORITHM, algorithm) if label is not None: msg += struct.pack( "!B%ds" % LABEL_LENGTH, LIST_FILTER.LABEL, _label_pack(label) ) resp = self.send_secure_cmd(COMMAND.LIST_OBJECTS, msg) objects = [] for i in range(0, len(resp), 4): obj_id, typ, seq = struct.unpack("!HBB", resp[i : i + 4]) objects.append(YhsmObject._create(typ, self, obj_id, seq)) return objects def get_object(self, object_id: int, object_type: OBJECT) -> YhsmObject: """Get a reference to a YhsmObject with the given id and type. The object returned will be a subclass of YhsmObject corresponding to the given object_type. :param object_id: The ID of the object to retrieve. :param object_type: The type of the object to retrieve. :return: An object reference. """ return YhsmObject._create(object_type, self, object_id) def get_pseudo_random(self, length: int) -> bytes: """Get bytes from YubiHSM PRNG. :param length: The number of bytes to return. :return: The requested number of random bytes. """ msg = struct.pack("!H", length) return self.send_secure_cmd(COMMAND.GET_PSEUDO_RANDOM, msg) def reset_device(self) -> None: """Perform a factory reset of the YubiHSM. Resets and reboots the YubiHSM, deletes all Objects and restores the default Authkey. """ try: if self.send_secure_cmd(COMMAND.RESET_DEVICE) != b"": raise YubiHsmInvalidResponseError("Non-empty response") except YubiHsmConnectionError: pass # Assume reset went well, it may interrupt the connection. self._sid = None self._key_enc = self._key_mac = self._key_rmac = b"" self._hsm.close() def get_log_entries(self, previous_entry: Optional[LogEntry] = None) -> LogData: """Get logs from the YubiHSM. This returns a tuple of the number of unlogged boot events, the number of unlogged authentication events, and the log entries from the YubiHSM. The chain of entry digests will be validated, starting from the first entry returned, or the one supplied as previous_entry. :param previous_entry: Entry to start verification against. :return: A tuple consisting of the number of unlogged boot and authentication events, and the list of log entries. """ resp = self.send_secure_cmd(COMMAND.GET_LOG_ENTRIES) boot, auth, num = struct.unpack("!HHB", resp[:5]) data = resp[5:] if len(data) != num * LogEntry.LENGTH: raise YubiHsmInvalidResponseError("Incorrect length") logs = [] for i in range(0, len(data), LogEntry.LENGTH): entry = LogEntry.parse(data[i : i + LogEntry.LENGTH]) if previous_entry: if not entry.validate(previous_entry): raise YubiHsmInvalidResponseError("Incorrect log digest") logs.append(entry) previous_entry = entry return LogData(boot, auth, logs) def set_log_index(self, index: int) -> None: """Clear logs to free up space for use with forced audit. :param index: The log entry index to clear up to (inclusive). """ msg = struct.pack("!H", index) if self.send_secure_cmd(COMMAND.SET_LOG_INDEX, msg) != b"": raise YubiHsmInvalidResponseError("Non-empty response") def put_option(self, option: OPTION, value: bytes) -> None: """Set the raw value of a YubiHSM device option. :param option: The OPTION to set. :param value: The value to set the OPTION to. """ msg = struct.pack("!BH", option, len(value)) + value if self.send_secure_cmd(COMMAND.SET_OPTION, msg) != b"": raise YubiHsmInvalidResponseError("Non-empty response") def get_option(self, option: OPTION) -> bytes: """Get the raw value of a YubiHSM device option. :param option: The OPTION to get. :return: The currently set value for the given OPTION """ msg = struct.pack("!B", option) return self.send_secure_cmd(COMMAND.GET_OPTION, msg) def set_force_audit(self, audit: AUDIT) -> None: """Set the FORCE_AUDIT mode of the YubiHSM. :param audit: The AUDIT mode to set. """ self.put_option(OPTION.FORCE_AUDIT, struct.pack("B", audit)) def get_force_audit(self) -> AUDIT: """Get the current setting for forced audit mode. :return: The AUDIT setting for FORCE_AUDIT. """ return AUDIT(self.get_option(OPTION.FORCE_AUDIT)[0]) def set_command_audit(self, commands: Mapping[COMMAND, AUDIT]) -> None: """Set audit mode of commands. Takes a dict of COMMAND -> AUDIT pairs and updates the audit settings for the commands given. :param commands: Settings to update. :Example: >>> session.set_comment_audit({ ... COMMAND.ECHO: AUDIT.OFF, ... COMMAND.LIST_OBJECTS: AUDIT.ON ... }) """ msg = b"".join(struct.pack("!BB", k, v) for (k, v) in commands.items()) self.put_option(OPTION.COMMAND_AUDIT, msg) def get_command_audit(self) -> Mapping[COMMAND, AUDIT]: """Get a mapping of all available commands and their audit settings. :return: Dictionary of COMMAND -> AUDIT pairs. """ resp = self.get_option(OPTION.COMMAND_AUDIT) ret = {} for i in range(0, len(resp), 2): cmd = resp[i] val = AUDIT(resp[i + 1]) try: ret[COMMAND(cmd)] = val except ValueError: ret[_UnknownCommand(cmd)] = val # type: ignore return ret def set_enabled_algorithms(self, algorithms: Mapping[ALGORITHM, bool]) -> None: """Set audit mode of commands. New in YubiHSM 2.2.0. Algorithms can only be toggled on a "fresh" device (after reset, before adding objects). Takes a dict of ALGORITHM -> bool pairs and updates the enabled algorithm settings for the algorithms given. :param algorithms: The algorithms to update. :Example: >>> session.set_enabled_algorithms({ ... ALGORITHM.RSA_2048: False, ... ALGORITHM.RSA_OAEP_SHA256_: True, ... }) """ msg = b"".join(struct.pack("!BB", k, v) for (k, v) in algorithms.items()) self.put_option(OPTION.ALGORITHM_TOGGLE, msg) def get_enabled_algorithms(self) -> Mapping[ALGORITHM, bool]: """Get the algorithms available, and whether or not they are enabled. :return: A mapping of algorithms, to whether or not they are enabled. """ try: resp = self.get_option(OPTION.ALGORITHM_TOGGLE) ret = {} for i in range(0, len(resp), 2): alg = resp[i] val = bool(resp[i + 1]) try: ret[ALGORITHM(alg)] = val except ValueError: ret[_UnknownAlgorithm(alg)] = val # type: ignore return ret except YubiHsmDeviceError as e: if e.code == ERROR.INVALID_DATA: supported = self._hsm.get_device_info().supported_algorithms return {alg: True for alg in supported} raise def set_fips_mode(self, mode: bool) -> None: """Set the FIPS mode of the YubiHSM. YubiHSM2 FIPS only. This can only be toggled on a "fresh" device (after reset, before adding objects). :param mode: Whether to be in FIPS compliant mode or not. """ self.put_option(OPTION.FIPS_MODE, struct.pack("!B", mode)) def get_fips_status(self) -> FIPS_STATUS: """Get the current FIPS status. YubiHSM2 FIPS only. :return: The FipsStatus value. """ return FIPS_STATUS(self.get_option(OPTION.FIPS_MODE)[0]) def get_fips_mode(self) -> bool: """Get the current setting for FIPS mode. YubiHSM2 FIPS only. :return: True if in FIPS mode, False if not. """ warnings.warn("Deprecated, use get_fips_status instead", DeprecationWarning) return bool(self.get_option(OPTION.FIPS_MODE)[0]) def __repr__(self): return "{0.__class__.__name__}(id={0._sid}, hsm={0._hsm})".format(self) yubihsm-3.1.1/yubihsm/defs.py0000644000000000000000000002677100000000000013053 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Named constants used in YubiHSM commands.""" from enum import IntEnum, IntFlag, unique from typing import Tuple from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec Version = Tuple[int, int, int] @unique class ERROR(IntEnum): """Error codes returned by the YubiHSM""" OK = 0x00 INVALID_COMMAND = 0x01 INVALID_DATA = 0x02 INVALID_SESSION = 0x03 AUTHENTICATION_FAILED = 0x04 SESSIONS_FULL = 0x05 SESSION_FAILED = 0x06 STORAGE_FAILED = 0x07 WRONG_LENGTH = 0x08 INSUFFICIENT_PERMISSIONS = 0x09 LOG_FULL = 0x0A OBJECT_NOT_FOUND = 0x0B INVALID_ID = 0x0C SSH_CA_CONSTRAINT_VIOLATION = 0x0E INVALID_OTP = 0x0F DEMO_MODE = 0x10 OBJECT_EXISTS = 0x11 ALGORITHM_DISABLED = 0x12 COMMAND_UNEXECUTED = 0xFF def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) @unique class COMMAND(IntEnum): """Commands available to send to the YubiHSM""" ECHO = 0x01 CREATE_SESSION = 0x03 AUTHENTICATE_SESSION = 0x04 SESSION_MESSAGE = 0x05 DEVICE_INFO = 0x06 RESET_DEVICE = 0x08 GET_DEVICE_PUBLIC_KEY = 0x0A CLOSE_SESSION = 0x40 GET_STORAGE_INFO = 0x041 PUT_OPAQUE = 0x42 GET_OPAQUE = 0x43 PUT_AUTHENTICATION_KEY = 0x44 PUT_ASYMMETRIC_KEY = 0x45 GENERATE_ASYMMETRIC_KEY = 0x46 SIGN_PKCS1 = 0x47 LIST_OBJECTS = 0x48 DECRYPT_PKCS1 = 0x49 EXPORT_WRAPPED = 0x4A IMPORT_WRAPPED = 0x4B PUT_WRAP_KEY = 0x4C GET_LOG_ENTRIES = 0x4D GET_OBJECT_INFO = 0x4E SET_OPTION = 0x4F GET_OPTION = 0x50 GET_PSEUDO_RANDOM = 0x51 PUT_HMAC_KEY = 0x52 SIGN_HMAC = 0x53 GET_PUBLIC_KEY = 0x54 SIGN_PSS = 0x55 SIGN_ECDSA = 0x56 DERIVE_ECDH = 0x57 DELETE_OBJECT = 0x58 DECRYPT_OAEP = 0x59 GENERATE_HMAC_KEY = 0x5A GENERATE_WRAP_KEY = 0x5B VERIFY_HMAC = 0x5C SIGN_SSH_CERTIFICATE = 0x5D PUT_TEMPLATE = 0x5E GET_TEMPLATE = 0x5F DECRYPT_OTP = 0x60 CREATE_OTP_AEAD = 0x61 RANDOMIZE_OTP_AEAD = 0x62 REWRAP_OTP_AEAD = 0x63 SIGN_ATTESTATION_CERTIFICATE = 0x64 PUT_OTP_AEAD_KEY = 0x65 GENERATE_OTP_AEAD_KEY = 0x66 SET_LOG_INDEX = 0x67 WRAP_DATA = 0x68 UNWRAP_DATA = 0x69 SIGN_EDDSA = 0x6A BLINK_DEVICE = 0x6B CHANGE_AUTHENTICATION_KEY = 0x6C PUT_SYMMETRIC_KEY = 0x6D GENERATE_SYMMETRIC_KEY = 0x6E DECRYPT_ECB = 0x6F ENCRYPT_ECB = 0x70 DECRYPT_CBC = 0x71 ENCRYPT_CBC = 0x72 PUT_PUBLIC_WRAP_KEY = 0x73 WRAP_KEY_RSA = 0x74 UNWRAP_KEY_RSA = 0x75 EXPORT_WRAPPED_RSA = 0x76 IMPORT_WRAPPED_RSA = 0x77 ERROR = 0x7F def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) @unique class ALGORITHM(IntEnum): """Various algorithm constants""" RSA_PKCS1_SHA1 = 1 RSA_PKCS1_SHA256 = 2 RSA_PKCS1_SHA384 = 3 RSA_PKCS1_SHA512 = 4 RSA_PSS_SHA1 = 5 RSA_PSS_SHA256 = 6 RSA_PSS_SHA384 = 7 RSA_PSS_SHA512 = 8 RSA_2048 = 9 RSA_3072 = 10 RSA_4096 = 11 RSA_OAEP_SHA1 = 25 RSA_OAEP_SHA256 = 26 RSA_OAEP_SHA384 = 27 RSA_OAEP_SHA512 = 28 RSA_MGF1_SHA1 = 32 RSA_MGF1_SHA256 = 33 RSA_MGF1_SHA384 = 34 RSA_MGF1_SHA512 = 35 EC_P256 = 12 EC_P384 = 13 EC_P521 = 14 EC_K256 = 15 EC_BP256 = 16 EC_BP384 = 17 EC_BP512 = 18 EC_ECDSA_SHA1 = 23 EC_ECDH = 24 HMAC_SHA1 = 19 HMAC_SHA256 = 20 HMAC_SHA384 = 21 HMAC_SHA512 = 22 AES128_CCM_WRAP = 29 OPAQUE_DATA = 30 OPAQUE_X509_CERTIFICATE = 31 TEMPLATE_SSH = 36 AES128_YUBICO_OTP = 37 AES128_YUBICO_AUTHENTICATION = 38 AES192_YUBICO_OTP = 39 AES256_YUBICO_OTP = 40 AES192_CCM_WRAP = 41 AES256_CCM_WRAP = 42 EC_ECDSA_SHA256 = 43 EC_ECDSA_SHA384 = 44 EC_ECDSA_SHA512 = 45 EC_ED25519 = 46 EC_P224 = 47 RSA_PKCS1_DECRYPT = 48 EC_P256_YUBICO_AUTHENTICATION = 49 AES128 = 50 AES192 = 51 AES256 = 52 AES_ECB = 53 AES_CBC = 54 AES_KWP = 55 def __str__(self): return repr(self) def to_curve(self) -> ec.EllipticCurve: """Return a Cryptography EC curve instance for a given member. :return: The corresponding curve. :rtype: cryptography.hazmat.primitives.ec. :Example: >>> isinstance(ALGORITHM.EC_P256.to_curve(), ec.SECP256R1) True """ return _curve_table[self]() # type: ignore @staticmethod def for_curve(curve: ec.EllipticCurve) -> "ALGORITHM": """Returns a member corresponding to a Cryptography curve instance. :Example: >>> ALGORITHM.for_curve(ec.SECP256R1()) == ALGORITHM.EC_P256 True """ curve_type = type(curve) for key, val in _curve_table.items(): if val == curve_type: return key raise ValueError("Unsupported curve type: %s" % curve.name) def to_key_size(self) -> int: """Return the expected size (in bytes) of a key corresponding to an algorithm. :return: The corresponding key size (in bytes) to an algorithm. :Example: >>> ALGORITHM.AES128.to_key_size() 16 """ return _key_size_table[self] def to_hash_algorithm(self) -> hashes.HashAlgorithm: """Return the cryptography hash algorithm object corresponding to the algorithm. :return The corresponding cryptography hash algorithm object. :Example: >>> ALGORITHM.HMAC_SHA1.to_hash_algorithm() hashes.SHA1 """ return _hash_table[self]() _curve_table = { ALGORITHM.EC_P224: ec.SECP224R1, ALGORITHM.EC_P256: ec.SECP256R1, ALGORITHM.EC_P384: ec.SECP384R1, ALGORITHM.EC_P521: ec.SECP521R1, ALGORITHM.EC_K256: ec.SECP256K1, ALGORITHM.EC_BP256: ec.BrainpoolP256R1, ALGORITHM.EC_BP384: ec.BrainpoolP384R1, ALGORITHM.EC_BP512: ec.BrainpoolP512R1, } _key_size_table = { ALGORITHM.AES128_CCM_WRAP: 16, ALGORITHM.AES192_CCM_WRAP: 24, ALGORITHM.AES256_CCM_WRAP: 32, ALGORITHM.HMAC_SHA1: 64, # Maximum key size ALGORITHM.HMAC_SHA256: 64, # Maximum key size ALGORITHM.HMAC_SHA384: 128, # Maximum key size ALGORITHM.HMAC_SHA512: 128, # Maximum key size ALGORITHM.AES128_YUBICO_OTP: 16, ALGORITHM.AES192_YUBICO_OTP: 24, ALGORITHM.AES256_YUBICO_OTP: 32, ALGORITHM.AES128: 16, ALGORITHM.AES192: 24, ALGORITHM.AES256: 32, ALGORITHM.RSA_2048: 256, ALGORITHM.RSA_3072: 384, ALGORITHM.RSA_4096: 512, } _hash_table = { ALGORITHM.HMAC_SHA1: hashes.SHA1, ALGORITHM.HMAC_SHA256: hashes.SHA256, ALGORITHM.HMAC_SHA384: hashes.SHA384, ALGORITHM.HMAC_SHA512: hashes.SHA512, } @unique class LIST_FILTER(IntEnum): """Keys for use to filter on in list_objects""" ID = 0x01 TYPE = 0x02 DOMAINS = 0x03 CAPABILITIES = 0x04 ALGORITHM = 0x05 LABEL = 0x06 def __str__(self): return repr(self) @unique class OBJECT(IntEnum): """YubiHSM object types""" OPAQUE = 0x01 AUTHENTICATION_KEY = 0x02 ASYMMETRIC_KEY = 0x03 WRAP_KEY = 0x04 HMAC_KEY = 0x05 TEMPLATE = 0x06 OTP_AEAD_KEY = 0x07 SYMMETRIC_KEY = 0x08 PUBLIC_WRAP_KEY = 0x09 def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) @unique class OPTION(IntEnum): """YubiHSM device options""" FORCE_AUDIT = 0x01 COMMAND_AUDIT = 0x03 ALGORITHM_TOGGLE = 0x04 FIPS_MODE = 0x05 def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) @unique class AUDIT(IntEnum): """Values for audit options""" OFF = 0x00 ON = 0x01 FIXED = 0x02 def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) @unique class FIPS_STATUS(IntEnum): """Values for FIPS status""" OFF = 0x00 ON = 0x01 PENDING = 0x03 def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) class _enum_prop: # Static property for use with enums. def __init__(self, getter): self.getter = getter def __get__(self, instance, cls): return self.getter(cls) @unique class CAPABILITY(IntFlag): """YubiHSM object capability flags""" GET_OPAQUE = 1 << 0x00 PUT_OPAQUE = 1 << 0x01 PUT_AUTHENTICATION_KEY = 1 << 0x02 PUT_ASYMMETRIC = 1 << 0x03 GENERATE_ASYMMETRIC_KEY = 1 << 0x04 SIGN_PKCS = 1 << 0x05 SIGN_PSS = 1 << 0x06 SIGN_ECDSA = 1 << 0x07 SIGN_EDDSA = 1 << 0x08 DECRYPT_PKCS = 1 << 0x09 DECRYPT_OAEP = 1 << 0x0A DERIVE_ECDH = 1 << 0x0B EXPORT_WRAPPED = 1 << 0x0C IMPORT_WRAPPED = 1 << 0x0D PUT_WRAP_KEY = 1 << 0x0E GENERATE_WRAP_KEY = 1 << 0x0F EXPORTABLE_UNDER_WRAP = 1 << 0x10 SET_OPTION = 1 << 0x11 GET_OPTION = 1 << 0x12 GET_PSEUDO_RANDOM = 1 << 0x13 PUT_HMAC_KEY = 1 << 0x14 GENERATE_HMAC_KEY = 1 << 0x15 SIGN_HMAC = 1 << 0x16 VERIFY_HMAC = 1 << 0x17 GET_LOG_ENTRIES = 1 << 0x18 SIGN_SSH_CERTIFICATE = 1 << 0x19 GET_TEMPLATE = 1 << 0x1A PUT_TEMPLATE = 1 << 0x1B RESET_DEVICE = 1 << 0x1C DECRYPT_OTP = 1 << 0x1D CREATE_OTP_AEAD = 1 << 0x1E RANDOMIZE_OTP_AEAD = 1 << 0x1F REWRAP_FROM_OTP_AEAD_KEY = 1 << 0x20 REWRAP_TO_OTP_AEAD_KEY = 1 << 0x21 SIGN_ATTESTATION_CERTIFICATE = 1 << 0x22 PUT_OTP_AEAD_KEY = 1 << 0x23 GENERATE_OTP_AEAD_KEY = 1 << 0x24 WRAP_DATA = 1 << 0x25 UNWRAP_DATA = 1 << 0x26 DELETE_OPAQUE = 1 << 0x27 DELETE_AUTHENTICATION_KEY = 1 << 0x28 DELETE_ASYMMETRIC_KEY = 1 << 0x29 DELETE_WRAP_KEY = 1 << 0x2A DELETE_HMAC_KEY = 1 << 0x2B DELETE_TEMPLATE = 1 << 0x2C DELETE_OTP_AEAD_KEY = 1 << 0x2D CHANGE_AUTHENTICATION_KEY = 1 << 0x2E PUT_SYMMETRIC_KEY = 1 << 0x2F GENERATE_SYMMETRIC_KEY = 1 << 0x30 DELETE_SYMMETRIC_KEY = 1 << 0x31 DECRYPT_ECB = 1 << 0x32 ENCRYPT_ECB = 1 << 0x33 DECRYPT_CBC = 1 << 0x34 ENCRYPT_CBC = 1 << 0x35 PUBLIC_WRAP_KEY_WRITE = 1 << 0x36 PUBLIC_WRAP_KEY_DELETE = 1 << 0x37 def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) @_enum_prop def NONE(cls) -> "CAPABILITY": return cls(0) # type: ignore @_enum_prop def ALL(cls) -> "CAPABILITY": return cls(sum(cls)) # type: ignore class ORIGIN(IntFlag): GENERATED = 0x01 IMPORTED = 0x02 IMPORTED_WRAPPED = 0x10 # Set in combination with GENERATED/IMPORTED def __repr__(self): return "<%s.%s: %s>" % (self.__class__.__name__, self._name_, hex(self)) def __str__(self): return repr(self) yubihsm-3.1.1/yubihsm/exceptions.py0000644000000000000000000000265100000000000014302 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Exceptions thrown by this library.""" from .defs import ERROR class YubiHsmError(Exception): """Baseclass for YubiHSM errors.""" class YubiHsmConnectionError(YubiHsmError): """The connection to the YubiHSM failed.""" class YubiHsmDeviceError(YubiHsmError): """The YubiHSM returned an error code. :param int code: The device error code. """ def __init__(self, code: int): self.code = ERROR(code) super(YubiHsmDeviceError, self).__init__( "{0.name} (error code 0x{0.value:02x})".format(self.code) ) class YubiHsmInvalidRequestError(YubiHsmError): """The request was not able to be sent to the YubiHSM.""" class YubiHsmInvalidResponseError(YubiHsmError): """The YubiHSM returned an unexpected response.""" class YubiHsmAuthenticationError(YubiHsmError): """Authentication failed.""" yubihsm-3.1.1/yubihsm/objects.py0000644000000000000000000020144600000000000013555 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Classes for interacting with objects on a YubiHSM.""" import copy import gzip import struct from dataclasses import dataclass from typing import ClassVar, NamedTuple, Optional, Type, TypeVar, Union from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, PublicFormat, ) from . import core from .defs import ALGORITHM, CAPABILITY, COMMAND, OBJECT, ORIGIN, Version from .exceptions import YubiHsmInvalidResponseError from .utils import password_to_key LABEL_LENGTH = 40 MAX_AES_PAYLOAD_SIZE = 2026 AES_BLOCK_SIZE = 16 RSA_PUBLIC_EXPONENT = 65537 RSA_SIZES = [ 2048, 3072, 4096, ] def _label_pack(label: Union[str, bytes]) -> bytes: """Pack a label into binary form.""" if isinstance(label, str): label = label.encode() if len(label) > LABEL_LENGTH: raise ValueError("Label must be no longer than %d bytes" % LABEL_LENGTH) return label def _label_unpack(packed: bytes) -> Union[str, bytes]: """Unpack a label from its binary form.""" try: return packed.split(b"\0", 2)[0].decode() except UnicodeDecodeError: # Not valid UTF-8 string, return the raw data. return packed def _calc_hash(data: bytes, hash: hashes.HashAlgorithm) -> bytes: if not isinstance(hash, Prehashed): digest = hashes.Hash(hash, backend=default_backend()) digest.update(data) data = digest.finalize() return data @dataclass(frozen=True) class ObjectInfo: """Data structure holding various information about an object. :ivar capabilities: The capabilities of the object. :ivar id: The ID of the object. :ivar size: The size of the object. :ivar domains: The set of domains the object belongs to. :ivar object_type: The type of the object. :ivar algorithm: The algorithm of the object. :ivar sequence: The sequence number of the object. :ivar origin: How the object was created/imported. :ivar label: The label of the object. :ivar delegated_capabilities: The set of delegated capabilities for the object. """ FORMAT: ClassVar[str] = "!QHHHBBBB%dsQ" % LABEL_LENGTH LENGTH: ClassVar[int] = struct.calcsize(FORMAT) capabilities: CAPABILITY id: int size: int domains: int object_type: OBJECT algorithm: ALGORITHM sequence: int origin: ORIGIN label: Union[str, bytes] delegated_capabilities: CAPABILITY @classmethod def parse(cls, value: bytes) -> "ObjectInfo": """Parse an ObjectInfo from its binary representation.""" data = list(struct.unpack(cls.FORMAT, value)) data[4] = OBJECT(data[4]) data[5] = ALGORITHM(data[5]) data[7] = ORIGIN(data[7]) data[8] = _label_unpack(data[8]) return cls(*data) def _get_bytes(c: x509.Certificate, oid: int) -> bytes: return c.extensions.get_extension_for_oid( x509.ObjectIdentifier(f"1.3.6.1.4.1.41482.4.{oid}") ).value.public_bytes() def _get_int(c: x509.Certificate, oid: int) -> int: return int.from_bytes(_get_bytes(c, oid)[2:], "big") T_AttestationExtensions = TypeVar( "T_AttestationExtensions", bound="AttestationExtensions" ) @dataclass class AttestationExtensions: """Base attestation extensions. :ivar firmware_version: YubiHSM firmware version. :ivar serial: YubiHSM serial number. """ firmware_version: Version serial: int @classmethod def parse( cls: Type[T_AttestationExtensions], certificate: x509.Certificate, *args ) -> T_AttestationExtensions: if cls == AttestationExtensions: # When called on the base class, identify which subclass to use try: _get_bytes(certificate, 3) return KeyAttestationExtensions.parse(certificate) # type: ignore except x509.ExtensionNotFound: return DeviceAttestationExtensions.parse(certificate) # type: ignore version: Version = tuple(_get_bytes(certificate, 1)[-3:]) # type: ignore serial = _get_int(certificate, 2) return cls(version, serial, *args) @dataclass class DeviceAttestationExtensions(AttestationExtensions): """Device attestation extensions. Available on YubiHSM FIPS only. :ivar fips_certificate: The FIPS certificate. """ fips_certificate: Optional[int] @classmethod def parse(cls, certificate: x509.Certificate, *args): # Available on YubiHSM FIPS only try: fips_certificate = _get_int(certificate, 10) except x509.ExtensionNotFound: fips_certificate = None return super(DeviceAttestationExtensions, cls).parse( certificate, fips_certificate ) @dataclass class KeyAttestationExtensions(AttestationExtensions): """Key attestation extensions. :ivar origin: The origin of the key. :ivar domains: The set of domains assigned to the key object. :ivar capabilities: The set of capabilities assigned to the key object. :ivar object_id: The ID of the key object. :ivar label: The label of the key object. :ivar fips_approved: (available on YubiHSM FIPS >= 2.4.1 only) True if the key attestation was generated in FIPS-approved mode. """ origin: ORIGIN domains: int capabilities: CAPABILITY object_id: int label: Union[str, bytes] fips_approved: Optional[bool] @classmethod def parse(cls, certificate: x509.Certificate, *args): """Extracts the attributes from an an attestation certificate.""" origin = ORIGIN(_get_int(certificate, 3)) domains = _get_int(certificate, 4) capabilities = CAPABILITY(_get_int(certificate, 5)) object_id = _get_int(certificate, 6) label = _label_unpack(_get_bytes(certificate, 9)[2:]) # Available on YubiHSM FIPS >= 2.4.1 only try: fips_approved = bool(_get_int(certificate, 12)) except x509.ExtensionNotFound: fips_approved = None return super(KeyAttestationExtensions, cls).parse( certificate, origin, domains, capabilities, object_id, label, fips_approved ) T_Object = TypeVar("T_Object", bound="YhsmObject") class YhsmObject: """A reference to an object stored in a YubiHSM. YubiHSM objects are uniquely identified by their type and ID combined. :ivar session: The session to use for YubiHSM communication. :ivar id: The ID of the object. :ivar object_type: The type of the object. """ object_type: ClassVar[OBJECT] def __init__( self, session: "core.AuthSession", object_id: int, seq: Optional[int] = None ): self.session = session self.id: int = object_id self._seq = seq def with_session(self: T_Object, session: "core.AuthSession") -> T_Object: """Get a copy of the object reference, using the given session. :param session: The session to use for the created reference. :return: A new reference to the object, associated wth the given session. """ other = copy.copy(self) other.session = session return other def get_info(self) -> ObjectInfo: """Read extended information about the object from the YubiHSM. :return: Information about the object. """ msg = struct.pack("!HB", self.id, self.object_type) resp = self.session.send_secure_cmd(COMMAND.GET_OBJECT_INFO, msg) try: return ObjectInfo.parse(resp) except ValueError: raise YubiHsmInvalidResponseError() def delete(self) -> None: """Delete the object from the YubiHSM. .. warning:: This action in irreversible. """ msg = struct.pack("!HB", self.id, self.object_type) if self.session.send_secure_cmd(COMMAND.DELETE_OBJECT, msg) != b"": raise YubiHsmInvalidResponseError() @staticmethod def _create( object_type: OBJECT, session: "core.AuthSession", object_id: int, seq: Optional[int] = None, ) -> "YhsmObject": """ Create instance of `object_type`. When object type is not recognized, _create constructs an instance of `_UnknownYhsmObject`. """ for cls in YhsmObject.__subclasses__(): if getattr(cls, "object_type", None) == object_type: return cls(session, object_id, seq) return _UnknownYhsmObject(object_type, session, object_id, seq) @classmethod def _from_command( cls: Type[T_Object], session: "core.AuthSession", cmd: COMMAND, data: bytes ) -> T_Object: ret = session.send_secure_cmd(cmd, data) return cls(session, struct.unpack("!H", ret)[0]) def __repr__(self): return "{0.__class__.__name__}(id={0.id})".format(self) class _UnknownYhsmObject(YhsmObject): """ _UnknownYhsmObject is a generic YhsmObject with `self.object_type` set to the specified `object_type` parameter. """ def __init__(self, object_type: OBJECT, *args, **kwargs): super(_UnknownYhsmObject, self).__init__(*args, **kwargs) self.object_type = object_type # type: ignore class Opaque(YhsmObject): """Object used to store arbitrary data on the YubiHSM. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.OPAQUE_DATA` - :class:`~yubihsm.defs.ALGORITHM.OPAQUE_X509_CERTIFICATE` """ object_type = OBJECT.OPAQUE @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, data: bytes, ) -> "Opaque": """Import an Opaque object into the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the object. :param data: The binary data to store. :return: A reference to the newly created object. """ if not data: raise ValueError("Cannot store empty data") msg = struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) msg += data return cls._from_command(session, COMMAND.PUT_OPAQUE, msg) def get(self) -> bytes: """Read the data of an Opaque object from the YubiHSM. :return: The data stored for the object. """ msg = struct.pack("!H", self.id) return self.session.send_secure_cmd(COMMAND.GET_OPAQUE, msg) @classmethod def put_certificate( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, certificate: x509.Certificate, compress: bool = False, ) -> "Opaque": """Import an X509 certificate into the YubiHSM as an Opaque. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param certificate: A certificate to import. :param compress: (optional) Compress the certificate. :return: A reference to the newly created object. """ encoded_cert = certificate.public_bytes(Encoding.DER) if compress: encoded_cert = gzip.compress(encoded_cert) return cls.put( session, object_id, label, domains, capabilities, ALGORITHM.OPAQUE_X509_CERTIFICATE, encoded_cert, ) def get_certificate(self) -> x509.Certificate: """Read an Opaque object from the YubiHSM, parsed as a certificate. :return: The certificate stored for the object. """ cert_data = self.get() try: # Try to decompress cert cert_data = gzip.decompress(cert_data) except gzip.BadGzipFile: pass return x509.load_der_x509_certificate(cert_data, default_backend()) class AuthenticationKey(YhsmObject): """Used to authenticate a session with the YubiHSM. AuthenticationKeys use two separate keys to mutually authenticate and set up a secure session with a YubiHSM. These two keys can either be given explicitly, or be derived from a password. """ object_type = OBJECT.AUTHENTICATION_KEY @classmethod def put_derived( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, delegated_capabilities: CAPABILITY, password: str, ) -> "AuthenticationKey": """Create an AuthenticationKey derived from a password. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param delegated_capabilities: The set of capabilities that the AuthenticationKey can give to objects created when authenticated using it. :param password: The password to derive raw keys from. :return: A reference to the newly created object. """ key_enc, key_mac = password_to_key(password) return cls.put( session, object_id, label, domains, capabilities, delegated_capabilities, key_enc, key_mac, ) @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, delegated_capabilities: CAPABILITY, key_enc: bytes, key_mac: bytes, ) -> "AuthenticationKey": """Create an AuthenticationKey by providing raw keys. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param delegated_capabilities: The set of capabilities that the AuthenticationKey can give to objects created when authenticated using it. :param key_enc: The raw encryption key. :param key_mac: The raw MAC key. :return: A reference to the newly created object. """ msg = struct.pack( "!H%dsHQBQ" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, ALGORITHM.AES128_YUBICO_AUTHENTICATION, delegated_capabilities, ) msg += key_enc + key_mac return cls._from_command(session, COMMAND.PUT_AUTHENTICATION_KEY, msg) @classmethod def put_public_key( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, delegated_capabilities: CAPABILITY, public_key: ec.EllipticCurvePublicKey, ) -> "AuthenticationKey": """Create an asymmetric AuthenticationKey by providing a public key :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param delegated_capabilities: The set of capabilities that the AuthenticationKey can give to objects created when authenticated using it. :param public_key: The public key to import. :return: A reference to the newly created object. """ if not isinstance(public_key.curve, ec.SECP256R1): raise ValueError("Unsupported curve") msg = struct.pack( "!H%dsHQBQ" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, ALGORITHM.EC_P256_YUBICO_AUTHENTICATION, delegated_capabilities, ) numbers = public_key.public_numbers() msg += int.to_bytes(numbers.x, public_key.key_size // 8, "big") msg += int.to_bytes(numbers.y, public_key.key_size // 8, "big") return cls._from_command(session, COMMAND.PUT_AUTHENTICATION_KEY, msg) def change_password(self, password: str) -> None: """Change the password used to authenticate a session. Changes the raw keys used for authentication, by deriving them from a password. :param password: The password to derive raw keys from. """ key_enc, key_mac = password_to_key(password) self.change_key(key_enc, key_mac) def change_key(self, key_enc: bytes, key_mac: bytes) -> None: """Change the raw keys used to authenticate a session. :param key_enc: The raw encryption key. :param key_mac: The raw MAC key. """ msg = ( struct.pack("!HB", self.id, ALGORITHM.AES128_YUBICO_AUTHENTICATION) + key_enc + key_mac ) resp = self.session.send_secure_cmd(COMMAND.CHANGE_AUTHENTICATION_KEY, msg) if struct.unpack("!H", resp)[0] != self.id: raise YubiHsmInvalidResponseError("Wrong ID returned") def change_public_key(self, public_key: ec.EllipticCurvePublicKey) -> None: """Change an asymmetric AuthenticationKey's public key :param public_key: The new public key. """ if not isinstance(public_key.curve, ec.SECP256R1): raise ValueError("Unsupported curve") msg = struct.pack("!HB", self.id, ALGORITHM.EC_P256_YUBICO_AUTHENTICATION) numbers = public_key.public_numbers() msg += int.to_bytes(numbers.x, public_key.key_size // 8, "big") msg += int.to_bytes(numbers.y, public_key.key_size // 8, "big") resp = self.session.send_secure_cmd(COMMAND.CHANGE_AUTHENTICATION_KEY, msg) if struct.unpack("!H", resp)[0] != self.id: raise YubiHsmInvalidResponseError("Wrong ID returned") class AsymmetricKey(YhsmObject): """Used to sign/decrypt data with the private key of an asymmetric key pair. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.RSA_2048` - :class:`~yubihsm.defs.ALGORITHM.RSA_3072` - :class:`~yubihsm.defs.ALGORITHM.RSA_4096` - :class:`~yubihsm.defs.ALGORITHM.EC_P224` - :class:`~yubihsm.defs.ALGORITHM.EC_P256` - :class:`~yubihsm.defs.ALGORITHM.EC_P384` - :class:`~yubihsm.defs.ALGORITHM.EC_P521` - :class:`~yubihsm.defs.ALGORITHM.EC_K256` - :class:`~yubihsm.defs.ALGORITHM.EC_BP256` - :class:`~yubihsm.defs.ALGORITHM.EC_BP384` - :class:`~yubihsm.defs.ALGORITHM.EC_BP512` - :class:`~yubihsm.defs.ALGORITHM.EC_ED25519` """ object_type = OBJECT.ASYMMETRIC_KEY @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, key, ) -> "AsymmetricKey": """Import a private key into the YubiHSM. RSA and EC keys can be created by using the cryptography APIs. You can then pass either a :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` , a :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` , or a :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey` as `key`. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param key: The private key to import. :return: A reference to the newly created object. """ if isinstance(key, rsa.RSAPrivateKeyWithSerialization): rsa_numbers = key.private_numbers() if rsa_numbers.public_numbers.e != RSA_PUBLIC_EXPONENT: raise ValueError("Unsupported public exponent") if key.key_size not in RSA_SIZES: raise ValueError("Unsupported key size") serialized = int.to_bytes( rsa_numbers.p, key.key_size // 8 // 2, "big" ) + int.to_bytes(rsa_numbers.q, key.key_size // 8 // 2, "big") algo = getattr(ALGORITHM, "RSA_%d" % key.key_size) elif isinstance(key, ec.EllipticCurvePrivateKeyWithSerialization): ec_numbers = key.private_numbers() serialized = int.to_bytes( ec_numbers.private_value, (key.curve.key_size + 7) // 8, "big" ) algo = ALGORITHM.for_curve(key.curve) elif isinstance(key, ed25519.Ed25519PrivateKey): serialized = key.private_bytes( Encoding.Raw, PrivateFormat.Raw, NoEncryption() ) algo = ALGORITHM.EC_ED25519 else: raise ValueError("Unsupported key") msg = ( struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algo, ) + serialized ) return cls._from_command(session, COMMAND.PUT_ASYMMETRIC_KEY, msg) @classmethod def generate( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, ) -> "AsymmetricKey": """Generate a new private key in the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the private key. :return: A reference to the newly created object. """ msg = struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) return cls._from_command(session, COMMAND.GENERATE_ASYMMETRIC_KEY, msg) def get_public_key(self): """Get the public key of the key pair. This will return either a :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` or a :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` depending on the algorithm of the key. Ed25519 keys will be returned as a Cryptography :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey` object if possible (requires Cryptography 2.6 or later), or an internal representation if not, either which can be serialized using the :func:`~yubihsm.eddsa.serialize_ed25519_public_key` function. :return: The public key of the key pair. """ msg = struct.pack("!H", self.id) ret = self.session.send_secure_cmd(COMMAND.GET_PUBLIC_KEY, msg) algo = ALGORITHM(ret[0]) raw_key = ret[1:] if algo in [ALGORITHM.RSA_2048, ALGORITHM.RSA_3072, ALGORITHM.RSA_4096]: num = int.from_bytes(raw_key, "big") return rsa.RSAPublicNumbers(e=RSA_PUBLIC_EXPONENT, n=num).public_key( backend=default_backend() ) elif algo in [ ALGORITHM.EC_P224, ALGORITHM.EC_P256, ALGORITHM.EC_P384, ALGORITHM.EC_P521, ALGORITHM.EC_K256, ALGORITHM.EC_BP256, ALGORITHM.EC_BP384, ALGORITHM.EC_BP512, ]: c_len = len(raw_key) // 2 x = int.from_bytes(raw_key[:c_len], "big") y = int.from_bytes(raw_key[c_len:], "big") return ec.EllipticCurvePublicNumbers( curve=algo.to_curve(), x=x, y=y ).public_key(backend=default_backend()) elif algo in [ALGORITHM.EC_ED25519]: return ed25519.Ed25519PublicKey.from_public_bytes(raw_key) else: raise TypeError("Invalid ALGORITHM") def get_certificate(self) -> x509.Certificate: """Get the X509 certificate associated with the key. An X509 certificate is associated with an asymmetric key if it is stored as an Opaque object with the same object ID as the key, and it has the :class:`~yubihsm.defs.ALGORITHM.OPAQUE_X509_CERTIFICATE` algorithm set. Equivalent to calling `Opaque(session, key_id).get_certificate()`. :return: The certificate stored for the object. """ return Opaque(self.session, self.id).get_certificate() def put_certificate( self, label: str, domains: int, capabilities: CAPABILITY, certificate: x509.Certificate, ) -> Opaque: """Store an X509 certificate associated with this key. Equivalent to calling `Opaque.put_certificate(session, key_id, ...)`. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param certificate: A certificate to import. :return: A reference to the newly created object. """ return Opaque.put_certificate( self.session, self.id, label, domains, capabilities, certificate ) def sign_ecdsa( self, data: bytes, hash: hashes.HashAlgorithm = hashes.SHA256(), length: int = 0 ) -> bytes: """Sign data using ECDSA. :param data: The data to sign. :param hash: (optional) The algorithm to use when hashing the data. :param length: (optional) length to pad/truncate the hash to. :return: The resulting signature. """ data = _calc_hash(data, hash) if not length: length = hash.digest_size msg = struct.pack("!H%ds" % length, self.id, data.rjust(length, b"\0")) return self.session.send_secure_cmd(COMMAND.SIGN_ECDSA, msg) def derive_ecdh(self, public_key: ec.EllipticCurvePublicKey) -> bytes: """Perform an ECDH key exchange as specified in SP 800-56A. :param public_key: The public key to use for the key exchange. :return: The resulting shared key. """ point = public_key.public_bytes(Encoding.X962, PublicFormat.UncompressedPoint) msg = struct.pack("!H", self.id) + point return self.session.send_secure_cmd(COMMAND.DERIVE_ECDH, msg) def sign_pkcs1v1_5( self, data: bytes, hash: hashes.HashAlgorithm = hashes.SHA256() ) -> bytes: """Sign data using RSASSA-PKCS1-v1_5. :param data: The data to sign. :param hash: (optional) The algorithm to use when hashing the data. :return: The resulting signature. """ data = _calc_hash(data, hash) msg = struct.pack("!H", self.id) + data return self.session.send_secure_cmd(COMMAND.SIGN_PKCS1, msg) def decrypt_pkcs1v1_5(self, data: bytes) -> bytes: """Decrypt data encrypted with RSAES-PKCS1-v1_5. :param data: The ciphertext to decrypt. :return: The decrypted plaintext. """ msg = struct.pack("!H", self.id) + data return self.session.send_secure_cmd(COMMAND.DECRYPT_PKCS1, msg) def sign_pss( self, data: bytes, salt_len: int, hash: hashes.HashAlgorithm = hashes.SHA256(), mgf_hash: hashes.HashAlgorithm = hashes.SHA256(), ) -> bytes: """Sign data using RSASSA-PSS with MGF1. :param data: The data to sign. :param salt_len: The length of the salt to use. :param hash: (optional) The algorithm to use when hashing the data. :param mgf_hash: (optional) The algorithm to use for MGF1. :return: The resulting signature. """ data = _calc_hash(data, hash) mgf = getattr(ALGORITHM, "RSA_MGF1_%s" % mgf_hash.name.upper()) msg = struct.pack("!HBH", self.id, mgf, salt_len) + data return self.session.send_secure_cmd(COMMAND.SIGN_PSS, msg) def decrypt_oaep( self, data: bytes, label: bytes = b"", hash: hashes.HashAlgorithm = hashes.SHA256(), mgf_hash: hashes.HashAlgorithm = hashes.SHA256(), ) -> bytes: """Decrypt data encrypted with RSAES-OAEP. :param data: The ciphertext to decrypt. :param label: (optional) OAEP label. :param hash: (optional) The algorithm to use when hashing the data. :param mgf_hash: (optional) The algorithm to use for MGF1. :return: The decrypted plaintext. """ digest = hashes.Hash(hash, backend=default_backend()) digest.update(label) mgf = getattr(ALGORITHM, "RSA_MGF1_%s" % mgf_hash.name.upper()) msg = struct.pack("!HB", self.id, mgf) + data + digest.finalize() return self.session.send_secure_cmd(COMMAND.DECRYPT_OAEP, msg) def sign_eddsa(self, data: bytes) -> bytes: """Sign data using EdDSA. :param data: The data to sign. :return: The resulting signature. """ msg = struct.pack("!H", self.id) + data return self.session.send_secure_cmd(COMMAND.SIGN_EDDSA, msg) def attest(self, attesting_key_id: int = 0) -> x509.Certificate: """Attest this asymmetric key. Creates an X509 certificate containing this key pair's public key, signed by the asymmetric key identified by the given ID. You also need a X509 certificate stored with the same ID as the attesting key in the YubiHSM, to be used as a template. :param attesting_key_id: (optional) The ID of the asymmetric key used to attest. If omitted, the built-in Yubico attestation key is used. :return: The attestation certificate. """ msg = struct.pack("!HH", self.id, attesting_key_id) resp = self.session.send_secure_cmd(COMMAND.SIGN_ATTESTATION_CERTIFICATE, msg) return x509.load_der_x509_certificate(resp, default_backend()) def sign_ssh_certificate( self, template_id: int, request: bytes, algorithm: ALGORITHM = ALGORITHM.RSA_PKCS1_SHA1, ) -> bytes: """Sign an SSH certificate request. :param template_id: The ID of the SSH TEMPLATE to use. :param request: The SSH certificate request. :return: The SSH certificate signature. """ msg = struct.pack("!HHB", self.id, template_id, algorithm) + request return self.session.send_secure_cmd(COMMAND.SIGN_SSH_CERTIFICATE, msg) class WrapKey(YhsmObject): """Used to import and export other objects under wrap. Asymmetric wrapkeys are only used for importing wrapped objects. To export objects under asymmetric wrap, use :class:`~yubihsm.objects.PublicWrapKey`. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.AES128_CCM_WRAP` - :class:`~yubihsm.defs.ALGORITHM.AES192_CCM_WRAP` - :class:`~yubihsm.defs.ALGORITHM.AES256_CCM_WRAP` - :class:`~yubihsm.defs.ALGORITHM.RSA_2048` - :class:`~yubihsm.defs.ALGORITHM.RSA_3072` - :class:`~yubihsm.defs.ALGORITHM.RSA_4096` """ object_type = OBJECT.WRAP_KEY @classmethod def generate( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, delegated_capabilities: CAPABILITY, ) -> "WrapKey": """Generate a new wrap key in the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the wrap key. :return: A reference to the newly created object. """ if algorithm not in [ ALGORITHM.AES128_CCM_WRAP, ALGORITHM.AES192_CCM_WRAP, ALGORITHM.AES256_CCM_WRAP, ALGORITHM.RSA_2048, ALGORITHM.RSA_3072, ALGORITHM.RSA_4096, ]: raise ValueError("Invalid algorithm") msg = struct.pack( "!H%dsHQBQ" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, delegated_capabilities, ) return cls._from_command(session, COMMAND.GENERATE_WRAP_KEY, msg) @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, delegated_capabilities: CAPABILITY, key: Union[bytes, rsa.RSAPrivateKey], ) -> "WrapKey": """Import a wrap key into the YubiHSM. Asymmetric keys can be imported using the cryptography API. You can then pass a :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` as `key`. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the wrap key. :param delegated_capabilities: The set of capabilities that the WrapKey can give to objects that it imports. :param key: The encryption key corresponding to the algorithm. :return: A reference to the newly created object. """ if algorithm not in [ ALGORITHM.AES128_CCM_WRAP, ALGORITHM.AES192_CCM_WRAP, ALGORITHM.AES256_CCM_WRAP, ALGORITHM.RSA_2048, ALGORITHM.RSA_3072, ALGORITHM.RSA_4096, ]: raise ValueError("Invalid algorithm") if isinstance(key, rsa.RSAPrivateKeyWithSerialization): rsa_numbers = key.private_numbers() if rsa_numbers.public_numbers.e != RSA_PUBLIC_EXPONENT: raise ValueError("Unsupported public exponent") if key.key_size not in RSA_SIZES: raise ValueError("Unsupported key size") algo = getattr(ALGORITHM, "RSA_%d" % key.key_size) if algo != algorithm: raise ValueError("Key does not match algorithm (%s)" % algorithm.name) key_data = int.to_bytes( rsa_numbers.p, key.key_size // 8 // 2, "big" ) + int.to_bytes(rsa_numbers.q, key.key_size // 8 // 2, "big") else: if len(key) != algorithm.to_key_size(): raise ValueError( "Key length (%d) not matching algorithm (%s)" % (len(key), algorithm.name) ) key_data = key msg = struct.pack( "!H%dsHQBQ" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, delegated_capabilities, ) msg += key_data return cls._from_command(session, COMMAND.PUT_WRAP_KEY, msg) def get_public_key(self) -> rsa.RSAPublicKey: """Get the public key of the wrapkey pair.""" msg = struct.pack("!HB", self.id, self.object_type) ret = self.session.send_secure_cmd(COMMAND.GET_PUBLIC_KEY, msg) raw_key = ret[1:] num = int.from_bytes(raw_key, "big") return rsa.RSAPublicNumbers(e=RSA_PUBLIC_EXPONENT, n=num).public_key( backend=default_backend() ) def wrap_data(self, data: bytes) -> bytes: """Wrap (encrypt) arbitrary data. :param data: The data to encrypt. :return: The encrypted data. """ msg = struct.pack("!H", self.id) + data return self.session.send_secure_cmd(COMMAND.WRAP_DATA, msg) def unwrap_data(self, data: bytes) -> bytes: """Unwrap (decrypt) arbitrary data. :param data: The encrypted data to decrypt. :return: The decrypted data. """ msg = struct.pack("!H", self.id) + data return self.session.send_secure_cmd(COMMAND.UNWRAP_DATA, msg) def export_wrapped(self, obj: YhsmObject, seed: bool = False) -> bytes: """Export an object under wrap. :param obj: The object to export. :param seed: (optional) Export key with seed. Only applicable for ed25519 key objects. :return: The encrypted object data. """ if seed: msg = struct.pack("!HBHB", self.id, obj.object_type, obj.id, 1) else: msg = struct.pack("!HBH", self.id, obj.object_type, obj.id) return self.session.send_secure_cmd(COMMAND.EXPORT_WRAPPED, msg) def import_wrapped(self, wrapped_obj: bytes) -> YhsmObject: """Import an object previously exported under wrap. :param wraped_obj: The encrypted object data. :return: A reference to the imported object. """ msg = struct.pack("!H", self.id) + wrapped_obj ret = self.session.send_secure_cmd(COMMAND.IMPORT_WRAPPED, msg) object_type, object_id = struct.unpack("!BH", ret) return YhsmObject._create(object_type, self.session, object_id) def import_wrapped_rsa( self, wrapped_obj: bytes, oaep_hash: hashes.HashAlgorithm = hashes.SHA256(), mgf_hash: hashes.HashAlgorithm = hashes.SHA256(), oaep_label: bytes = b"", ) -> YhsmObject: """Import an object previously exported under asymmetric wrap. :param wrapped_obj: The encrypted object data. :param oaep_hash: (optional) The hash algorithm to use for OAEP label. :param mgf_hash: (optional) The hash algorithm to use for MGF1. :param oaep_label: (optional) OAEP label. :return: A reference to the imported object. """ digest = hashes.Hash(oaep_hash, backend=default_backend()) digest.update(oaep_label) hash = getattr(ALGORITHM, "RSA_OAEP_%s" % oaep_hash.name.upper()) mgf = getattr(ALGORITHM, "RSA_MGF1_%s" % mgf_hash.name.upper()) msg = struct.pack("!HBB", self.id, hash, mgf) + wrapped_obj + digest.finalize() ret = self.session.send_secure_cmd(COMMAND.IMPORT_WRAPPED_RSA, msg) object_type, object_id = struct.unpack("!BH", ret) return YhsmObject._create(object_type, self.session, object_id) def import_raw_key( self, object_id: int, object_type: OBJECT, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, wrapped: bytes, oaep_hash: hashes.HashAlgorithm = hashes.SHA256(), mgf_hash: hashes.HashAlgorithm = hashes.SHA256(), oaep_label: bytes = b"", ) -> YhsmObject: """Import an (a)symmetric key previously exported under asymmetric wrap. Asymmetric keys are expected to have been serialized as PKCS#8. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param object_type: The key object type (`OBJECT.ASYMMETRIC_KEY` or `OBJECT.SYMMETRIC_KEY`). :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm of the key. :param wrapped: The wrapped key object. :param oaep_hash: (optional) The hash algorithm to use for OAEP label. :param mgf_hash: (optional) The hash algorithm to use for MGF1. :param oaep_label: (optional) OAEP label. :return: A reference to the imported key object. """ digest = hashes.Hash(oaep_hash, backend=default_backend()) digest.update(oaep_label) hash = getattr(ALGORITHM, "RSA_OAEP_%s" % oaep_hash.name.upper()) mgf = getattr(ALGORITHM, "RSA_MGF1_%s" % mgf_hash.name.upper()) msg = ( struct.pack( "!HBH%dsHQBBB" % LABEL_LENGTH, self.id, object_type, object_id, _label_pack(label), domains, capabilities, algorithm, hash, mgf, ) + wrapped + digest.finalize() ) ret = self.session.send_secure_cmd(COMMAND.UNWRAP_KEY_RSA, msg) object_type, object_id = struct.unpack("!BH", ret) return YhsmObject._create(object_type, self.session, object_id) class PublicWrapKey(YhsmObject): """Used to export other objects under wrap using the public key of an asymmetric key pair. The algorithm used for wrapping is CKM_RSA_AES_KEY_WRAP, as specified in PKCS#11. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.RSA_2048` - :class:`~yubihsm.defs.ALGORITHM.RSA_3072` - :class:`~yubihsm.defs.ALGORITHM.RSA_4096` """ object_type = OBJECT.PUBLIC_WRAP_KEY @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, delegated_capabilities: CAPABILITY, public_key: rsa.RSAPublicKey, ) -> "PublicWrapKey": """Import a public RSA wrapkey into the YubiHSM. The RSA public key can be supplied using the cryptography API. You can then pass a :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` as `public_key`. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param delegated_capabilities: The set of capabilities that the WrapKey can give to objects that it imports. :param public_key: The public key to import. :return: A reference to the newly created object. """ algorithm = getattr(ALGORITHM, "RSA_%d" % public_key.key_size) if algorithm not in [ ALGORITHM.RSA_2048, ALGORITHM.RSA_3072, ALGORITHM.RSA_4096, ]: raise ValueError("Invalid algorithm") public_numbers = public_key.public_numbers() if public_numbers.e != RSA_PUBLIC_EXPONENT: raise ValueError("Unsupported public exponent") serialized = public_numbers.n.to_bytes((public_key.key_size + 7) // 8, "big") msg = ( struct.pack( "!H%dsHQBQ" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, delegated_capabilities, ) + serialized ) return cls._from_command(session, COMMAND.PUT_PUBLIC_WRAP_KEY, msg) def get_public_key(self) -> rsa.RSAPublicKey: """Get the public wrapkey.""" msg = struct.pack("!HB", self.id, self.object_type) ret = self.session.send_secure_cmd(COMMAND.GET_PUBLIC_KEY, msg) raw_key = ret[1:] num = int.from_bytes(raw_key, "big") return rsa.RSAPublicNumbers(e=RSA_PUBLIC_EXPONENT, n=num).public_key( backend=default_backend() ) def _rsa_wrap_cmd_data( self, obj: YhsmObject, algorithm: ALGORITHM, oaep_hash: hashes.HashAlgorithm, mgf_hash: hashes.HashAlgorithm, oaep_label: bytes, ) -> bytes: digest = hashes.Hash(oaep_hash, backend=default_backend()) digest.update(oaep_label) hash = getattr(ALGORITHM, "RSA_OAEP_%s" % oaep_hash.name.upper()) mgf = getattr(ALGORITHM, "RSA_MGF1_%s" % mgf_hash.name.upper()) msg = ( struct.pack( "!HBHBBB", self.id, obj.object_type, obj.id, algorithm, hash, mgf, ) + digest.finalize() ) return msg def export_wrapped_rsa( self, obj: YhsmObject, algorithm: ALGORITHM = ALGORITHM.AES256, oaep_hash: hashes.HashAlgorithm = hashes.SHA256(), mgf_hash: hashes.HashAlgorithm = hashes.SHA256(), oaep_label: bytes = b"", ) -> bytes: """Export an object under asymmetric wrap. :param obj: The object to export. :param algorithm: (optional) The algorithm to use for the ephemeral key. :param oaep_hash: (optional) The hash algorithm to use for OAEP label. :param mgf_hash: (optional) The hash algorithm to use for MGF1. :param oaep_label: (optional) OAEP label. :return: The encrypted object data. """ msg = self._rsa_wrap_cmd_data( obj, algorithm, oaep_hash, mgf_hash, oaep_label, ) return self.session.send_secure_cmd(COMMAND.EXPORT_WRAPPED_RSA, msg) def export_raw_key( self, key: Union[AsymmetricKey, "SymmetricKey"], algorithm: ALGORITHM = ALGORITHM.AES256, oaep_hash: hashes.HashAlgorithm = hashes.SHA256(), mgf_hash: hashes.HashAlgorithm = hashes.SHA256(), oaep_label: bytes = b"", ) -> bytes: """Export an (a)symmetric key object under asymmetric wrap. This command wraps only the raw key material of the key object. Asymmetric keys are serialized as PKCS#8. :param key: The (a)symmetric key object to wrap. :param algorithm: (optional) The algorithm for the ephemeral key. :param oaep_hash: (optional) The hash algorithm to use for OAEP label. :param mgf_hash: (optional) The hash algorithm to use for MGF1. :param oaep_label: (optional) OAEP label. :return: The encrypted key. """ msg = self._rsa_wrap_cmd_data( key, algorithm, oaep_hash, mgf_hash, oaep_label, ) return self.session.send_secure_cmd(COMMAND.WRAP_KEY_RSA, msg) class HmacKey(YhsmObject): """Used to calculate and verify HMAC signatures. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.HMAC_SHA1` - :class:`~yubihsm.defs.ALGORITHM.HMAC_SHA256` - :class:`~yubihsm.defs.ALGORITHM.HMAC_SHA384` - :class:`~yubihsm.defs.ALGORITHM.HMAC_SHA512` """ object_type = OBJECT.HMAC_KEY @classmethod def generate( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM = ALGORITHM.HMAC_SHA256, ) -> "HmacKey": """Generate a new HMAC key in the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: (optional) The algorithm to use for the HMAC key. :return: A reference to the newly created object. """ if algorithm not in [ ALGORITHM.HMAC_SHA1, ALGORITHM.HMAC_SHA256, ALGORITHM.HMAC_SHA384, ALGORITHM.HMAC_SHA512, ]: raise ValueError("Invalid algorithm") msg = struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) return cls._from_command(session, COMMAND.GENERATE_HMAC_KEY, msg) @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, key: bytes, algorithm=ALGORITHM.HMAC_SHA256, ) -> "HmacKey": """Import an HMAC key into the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param key: The raw key corresponding to the algorithm. :param algorithm: (optional) The algorithm to use for the HMAC key. :return: A reference to the newly created object. """ if algorithm not in [ ALGORITHM.HMAC_SHA1, ALGORITHM.HMAC_SHA256, ALGORITHM.HMAC_SHA384, ALGORITHM.HMAC_SHA512, ]: raise ValueError("Invalid algorithm") if len(key) > algorithm.to_key_size(): # Hash key using corresponding hash algorithm key = _calc_hash(key, algorithm.to_hash_algorithm()) msg = ( struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) + key ) return cls._from_command(session, COMMAND.PUT_HMAC_KEY, msg) def sign_hmac(self, data: bytes) -> bytes: """Calculate the HMAC signature of the given data. :param data: The data to sign. :return: The signature. """ msg = struct.pack("!H", self.id) + data return self.session.send_secure_cmd(COMMAND.SIGN_HMAC, msg) def verify_hmac(self, signature: bytes, data: bytes) -> bool: """Verify an HMAC signature. :param signature: The signature to verify. :param data: The data to verify the signature against. :return: True if verification succeeded, False if not. """ msg = struct.pack("!H", self.id) + signature + data return self.session.send_secure_cmd(COMMAND.VERIFY_HMAC, msg) == b"\1" class Template(YhsmObject): """Binary template used to validate SSH certificate requests. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.TEMPLATE_SSH` """ object_type = OBJECT.TEMPLATE @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, data: bytes, ) -> "Template": """Import a Template into the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the template. :param data: The template data. :return: A reference to the newly created object. """ msg = struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) msg += data return cls._from_command(session, COMMAND.PUT_TEMPLATE, msg) def get(self) -> bytes: """Read a Template from the YubiHSM. :return: The template data. """ msg = struct.pack("!H", self.id) return self.session.send_secure_cmd(COMMAND.GET_TEMPLATE, msg) class OtpData(NamedTuple): """Decrypted OTP counter values. :param use_counter: 16 bit counter incremented on each power cycle. :param session_counter: 8 bit counter incremented on each touch. :param timestamp_high: 8 bit high part of the timestamp. :param timestamp_low: 16 bit low part of the timestamp. """ use_counter: int session_counter: int timestamp_high: int timestamp_low: int class OtpAeadKey(YhsmObject): """Used to decrypt and use a Yubico OTP AEAD for OTP decryption. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.AES128_YUBICO_OTP` - :class:`~yubihsm.defs.ALGORITHM.AES192_YUBICO_OTP` - :class:`~yubihsm.defs.ALGORITHM.AES256_YUBICO_OTP` """ object_type = OBJECT.OTP_AEAD_KEY @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, nonce_id: int, key: bytes, ) -> "OtpAeadKey": """Import an OTP AEAD key into the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the key. :param nonce_id: The nonce ID used for AEADs. :param key: The key to import, corresponding to the algorithm. :return: A reference to the newly created object. """ if algorithm not in [ ALGORITHM.AES128_YUBICO_OTP, ALGORITHM.AES192_YUBICO_OTP, ALGORITHM.AES256_YUBICO_OTP, ]: raise ValueError("Invalid algorithm") if len(key) != algorithm.to_key_size(): raise ValueError( "Key length (%d) not matching algorithm (%s)" % (len(key), algorithm.name) ) msg = struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) + struct.pack(" "OtpAeadKey": """Generate a new OTP AEAD key in the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the key. :param nonce_id: The nonce ID used for AEADs. :return: A reference to the newly created object. """ if algorithm not in [ ALGORITHM.AES128_YUBICO_OTP, ALGORITHM.AES192_YUBICO_OTP, ALGORITHM.AES256_YUBICO_OTP, ]: raise ValueError("Invalid algorithm") msg = struct.pack( "!H%dsHQBL" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, nonce_id, ) return cls._from_command(session, COMMAND.GENERATE_OTP_AEAD_KEY, msg) def create_otp_aead(self, key: bytes, identity: bytes) -> bytes: """Create a new Yubico OTP credential AEAD. :param key: 16 byte AES key for the credential. :param identity: 6 byte private ID for the credential. :return: A new AEAD. """ msg = struct.pack("!H", self.id) + key + identity return self.session.send_secure_cmd(COMMAND.CREATE_OTP_AEAD, msg) def randomize_otp_aead(self) -> bytes: """Create a new Yubico OTP credential AEAD using random data. :return: A new AEAD. """ msg = struct.pack("!H", self.id) return self.session.send_secure_cmd(COMMAND.RANDOMIZE_OTP_AEAD, msg) def decrypt_otp(self, aead: bytes, otp: bytes) -> OtpData: """Decrypt a Yubico OTP using an AEAD. :param aead: The AEAD containing encrypted credential data. :param otp: The 16 byte encrypted OTP payload to decrypt. :return: The decrypted OTP data. """ msg = struct.pack("!H", self.id) + aead + otp resp = self.session.send_secure_cmd(COMMAND.DECRYPT_OTP, msg) return OtpData(*struct.unpack(" bytes: """Decrypt and re-encrypt an AEAD from one key to another. :param new_key_id: The ID of the OtpAeadKey to wrap to. :param aead: The AEAD to re-wrap. :return: The new AEAD. """ msg = struct.pack("!HH", self.id, new_key_id) + aead return self.session.send_secure_cmd(COMMAND.REWRAP_OTP_AEAD, msg) class SymmetricKey(YhsmObject): """Used to encrypt/decrypt data using a symmetric key. Supported algorithms: - :class:`~yubihsm.defs.ALGORITHM.AES128` - :class:`~yubihsm.defs.ALGORITHM.AES192` - :class:`~yubihsm.defs.ALGORITHM.AES256` """ object_type = OBJECT.SYMMETRIC_KEY @classmethod def put( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, key: bytes, ) -> "SymmetricKey": """Import a symmetric key into the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the symmetric key. :param key: The raw encryption key corresponding to the algorithm. :return: A reference to the newly created object. """ if algorithm not in [ALGORITHM.AES128, ALGORITHM.AES192, ALGORITHM.AES256]: raise ValueError("Invalid algorithm") if len(key) != algorithm.to_key_size(): raise ValueError( "Key length (%d) not matching algorithm (%s)" % (len(key), algorithm.name) ) msg = struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) msg += key return cls._from_command(session, COMMAND.PUT_SYMMETRIC_KEY, msg) @classmethod def generate( cls, session: "core.AuthSession", object_id: int, label: str, domains: int, capabilities: CAPABILITY, algorithm: ALGORITHM, ) -> "SymmetricKey": """Generate a new symmetric key in the YubiHSM. :param session: The session to import via. :param object_id: The ID to set for the object. Set to 0 to let the YubiHSM designate an ID. :param label: A text label to give the object. :param domains: The set of domains to assign the object to. :param capabilities: The set of capabilities to give the object. :param algorithm: The algorithm to use for the symmetric key. :return: A reference to the newly created object. """ if algorithm not in [ALGORITHM.AES128, ALGORITHM.AES192, ALGORITHM.AES256]: raise ValueError("Invalid algorithm") msg = struct.pack( "!H%dsHQB" % LABEL_LENGTH, object_id, _label_pack(label), domains, capabilities, algorithm, ) return cls._from_command(session, COMMAND.GENERATE_SYMMETRIC_KEY, msg) def _chain_ecb(self, cmd: COMMAND, data: bytes) -> bytes: if len(data) % AES_BLOCK_SIZE != 0: raise ValueError("Data is not a multiple of %d bytes" % AES_BLOCK_SIZE) chunk_size = MAX_AES_PAYLOAD_SIZE // AES_BLOCK_SIZE * AES_BLOCK_SIZE out = b"" rem = data while rem: if len(rem) <= chunk_size: chunk_in = rem rem = b"" else: chunk_in = rem[:chunk_size] rem = rem[chunk_size:] msg = struct.pack("!H", self.id) + chunk_in chunk_out = self.session.send_secure_cmd(cmd, msg) out += chunk_out return out def _chain_cbc(self, cmd: COMMAND, iv: bytes, data: bytes) -> bytes: if len(iv) != AES_BLOCK_SIZE: raise ValueError("IV is not 16 bytes") if len(data) % AES_BLOCK_SIZE != 0: raise ValueError("Data is not a multiple of %d bytes" % AES_BLOCK_SIZE) chunk_size = (MAX_AES_PAYLOAD_SIZE - len(iv)) // AES_BLOCK_SIZE * AES_BLOCK_SIZE out = b"" rem = data while rem: if len(rem) <= chunk_size: chunk_in = rem rem = b"" else: chunk_in = rem[:chunk_size] rem = rem[chunk_size:] msg = struct.pack("!H", self.id) + iv + chunk_in chunk_out = self.session.send_secure_cmd(cmd, msg) out += chunk_out iv = ( out[-AES_BLOCK_SIZE:] if cmd == COMMAND.ENCRYPT_CBC else chunk_in[-AES_BLOCK_SIZE:] ) return out def encrypt_ecb(self, data: bytes) -> bytes: """Encrypt data in ECB mode. :param data: The data to encrypt. :return: The encrypted data. """ return self._chain_ecb(COMMAND.ENCRYPT_ECB, data) def decrypt_ecb(self, data: bytes) -> bytes: """Decrypt data in ECB mode. :param data: The data to decrypt. :return: The decrypted data. """ return self._chain_ecb(COMMAND.DECRYPT_ECB, data) def encrypt_cbc(self, iv: bytes, data: bytes) -> bytes: """Encrypt data in CBC mode. :param iv: The initialization vector. :param data: The data to encrypt. :return: The encrypted data. """ return self._chain_cbc(COMMAND.ENCRYPT_CBC, iv, data) def decrypt_cbc(self, iv: bytes, data: bytes) -> bytes: """Decrypt data in CBC mode. :param iv: The initialization vector. :param data: The data to decrypt. :return: The decrypted data. """ return self._chain_cbc(COMMAND.DECRYPT_CBC, iv, data) yubihsm-3.1.1/yubihsm/py.typed0000644000000000000000000000000000000000000013230 0ustar00yubihsm-3.1.1/yubihsm/utils.py0000644000000000000000000000246500000000000013264 0ustar00# Copyright 2016-2018 Yubico AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Various utility functions used throughout the library.""" from typing import Tuple from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC def password_to_key(password: str) -> Tuple[bytes, bytes]: """Derive keys for establishing a YubiHSM session from a password. :return: A tuple containing the encryption key, and MAC key. """ pw_bytes = password.encode() key = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=b"Yubico", iterations=10000, backend=default_backend(), ).derive(pw_bytes) key_enc, key_mac = key[:16], key[16:] return key_enc, key_mac yubihsm-3.1.1/PKG-INFO0000644000000000000000000004365300000000000011173 0ustar00Metadata-Version: 2.3 Name: yubihsm Version: 3.1.1 Summary: Library for communication with a YubiHSM 2 over HTTP or USB. License: Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Author: Dain Nilsson Author-email: Requires-Python: >=3.9 Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Software Development :: Libraries Provides-Extra: http Provides-Extra: usb Requires-Dist: cryptography (>=2.6,<47) Requires-Dist: pyusb (>=1.0,<2.0) ; extra == "usb" Requires-Dist: requests (>=2.0,<3.0) ; extra == "http" Project-URL: Homepage, https://developers.yubico.com/YubiHSM2/ Project-URL: Repository, https://github.com/Yubico/python-yubihsm Description-Content-Type: text/plain == python-yubihsm Python library and tests for the YubiHSM 2. The current version (3.0) supports Python 3.9 and later. Communicates with the YubiHSM 2 connector daemon, which must already be running. It can also communicate directly with the YubiHSM 2 via USB (requires libusb). === License .... Copyright 2023 Yubico AB Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .... === Installation From PyPI: $ pip install yubihsm[http,usb] From a source .tar.gz: $ pip install yubihsm-.tar.gz[http,usb] Omitting a tag from the brackets will install the library without support for that backend, and will avoid installing unneeded dependencies. === Quick reference commands: [source,python] ---- from yubihsm import YubiHsm from yubihsm.defs import CAPABILITY, ALGORITHM from yubihsm.objects import AsymmetricKey from cryptography.hazmat.primitives import serialization # Connect to the YubiHSM via the connector using the default password: hsm = YubiHsm.connect('http://localhost:12345') session = hsm.create_session_derived(1, 'password') # Generate a private key on the YubiHSM for creating signatures: key = AsymmetricKey.generate( # Generate a new key object in the YubiHSM. session, # Secure YubiHsm session to use. 0, # Object ID, 0 to get one assigned. 'My key', # Label for the object. 1, # Domain(s) for the object. CAPABILITY.SIGN_ECDSA, # Capabilities for the object, can have multiple. ALGORITHM.EC_P256 # Algorithm for the key. ) # pub_key is a cryptography.io ec.PublicKey, see https://cryptography.io pub_key = key.get_public_key() # Write the public key to a file: with open('public_key.pem', 'wb') as f: f.write(pub_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo )) # Sign some data: signature = key.sign_ecdsa(b'Hello world!') # Create a signature. # Clean up: session.close() hsm.close() ---- === Development This project uses https://docs.astral.sh/uv/[uv] for development. Follow the uv Getting Started guide to install and configure it. When `uv` is installed and configured you can set up the dev environment for this project by running the following command in the root directory of the repository: $ uv sync --all-extras ==== Pre-commit checks This project uses https://pre-commit.com to run several checks on the code prior to committing. To enable the hooks, run these commands in the root directory of the repository: $ uv tool install pre-commit $ pre-commit install Once the hooks are installed, they will run automatically on any changed files when committing. To run the hooks against all files in the repository, run: $ pre-commit run --all-files ==== Running tests Running the tests require a YubiHSM2 to run against, with the default authentication key enabled (as is the case after performing a factory reset). WARNING: The YubiHSM under test will be factory reset by the tests! $ uv run pytest See pytest documentation for instructions on running a specific test. By default the tests will connect to a yubihsm-connector running with the default settings on http://localhost:12345. To change this, use the `--backend` argument, eg: $ uv run pytest --backend "yhusb://" Access to the device requires proper permissions, so either use sudo or setup a udev rule. Sample udev configuration can be found link:https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-connector/[here]. ==== Generating HTML documentation To build the HTML documentation, run: $ uv run make -C docs/ html The resulting output will be in docs/_build/html/. ==== Source releases for distribution Build a source release: $ uv build The resulting .tar.gz and .whl will be created in `dist/`.