├── .github ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGES.rst ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cwt ├── __init__.py ├── algs │ ├── __init__.py │ ├── asymmetric.py │ ├── ec2.py │ ├── non_aead.py │ ├── okp.py │ ├── raw.py │ ├── rsa.py │ └── symmetric.py ├── cbor_processor.py ├── claims.py ├── const.py ├── cose.py ├── cose_key.py ├── cose_key_interface.py ├── cose_message.py ├── cwt.py ├── encrypted_cose_key.py ├── enums.py ├── exceptions.py ├── helpers │ ├── __init__.py │ └── hcert.py ├── py.typed ├── recipient.py ├── recipient_algs │ ├── __init__.py │ ├── aes_key_wrap.py │ ├── direct.py │ ├── direct_hkdf.py │ ├── direct_key.py │ ├── ecdh_aes_key_wrap.py │ ├── ecdh_direct_hkdf.py │ └── hpke.py ├── recipient_interface.py ├── recipients.py ├── signer.py └── utils.py ├── docs ├── Makefile ├── algorithms.rst ├── api.rst ├── changes.rst ├── claims.rst ├── conf.py ├── index.rst └── installation.rst ├── poetry.lock ├── pyproject.toml ├── samples ├── __init__.py └── eudcc │ ├── __init__.py │ ├── swedish_verifier.py │ └── verifier.py ├── tests ├── __init__.py ├── keys │ ├── cacert.pem │ ├── cacert_2.pem │ ├── cakey.pem │ ├── cert_es256.pem │ ├── certs │ │ ├── ca.crt │ │ ├── ca.pem │ │ ├── ca_another.crt │ │ ├── ca_another.pem │ │ ├── ca_key.pem │ │ ├── create_certs.sh │ │ ├── openssl_ca.cnf │ │ ├── openssl_server.cnf │ │ ├── server.crt │ │ ├── server.json │ │ ├── server.pem │ │ ├── server_key.pem │ │ └── server_without_root.json │ ├── hcert_testdata_cert_at.pem │ ├── hs256.json │ ├── hs384.json │ ├── hs512.json │ ├── private_key_cert_es256.pem │ ├── private_key_ed25519.json │ ├── private_key_ed25519.pem │ ├── private_key_ed448.json │ ├── private_key_ed448.pem │ ├── private_key_es256.json │ ├── private_key_es256.pem │ ├── private_key_es256k.json │ ├── private_key_es256k.pem │ ├── private_key_es384.json │ ├── private_key_es384.pem │ ├── private_key_es512.json │ ├── private_key_es512.pem │ ├── private_key_rsa.json │ ├── private_key_rsa.pem │ ├── private_key_x25519.json │ ├── private_key_x25519.pem │ ├── private_key_x448.json │ ├── private_key_x448.pem │ ├── public_key_ed25519.json │ ├── public_key_ed25519.pem │ ├── public_key_ed448.json │ ├── public_key_ed448.pem │ ├── public_key_es256.json │ ├── public_key_es256.pem │ ├── public_key_es256k.json │ ├── public_key_es256k.pem │ ├── public_key_es384.json │ ├── public_key_es384.pem │ ├── public_key_es512.json │ ├── public_key_es512.pem │ ├── public_key_rsa.json │ ├── public_key_rsa.pem │ ├── public_key_x25519.json │ ├── public_key_x25519.pem │ ├── public_key_x448.json │ └── public_key_x448.pem ├── test_algs_aes_key_wrap.py ├── test_algs_ec2.py ├── test_algs_okp.py ├── test_algs_raw.py ├── test_algs_rsa.py ├── test_algs_symmetric.py ├── test_claims.py ├── test_cose.py ├── test_cose_hpke.py ├── test_cose_key.py ├── test_cose_message.py ├── test_cose_sample.py ├── test_cose_sample_with_encode.py ├── test_cose_wg_examples.py ├── test_cwt.py ├── test_cwt_sample.py ├── test_deterministic_encoding.py ├── test_encrypted_cose_key.py ├── test_helpers_hcert.py ├── test_key.py ├── test_recipient.py ├── test_recipient_algs_aes_key_wrap.py ├── test_recipient_algs_direct.py ├── test_recipient_algs_ecdh_aes_key_wrap.py ├── test_recipient_algs_ecdh_direct_hkdf.py ├── test_recipient_algs_hpke.py ├── test_signer.py ├── test_utils.py └── utils.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml file with 2 | # minimum configuration for two package managers 3 | 4 | version: 2 5 | updates: 6 | # Enable version updates for npm 7 | - package-ecosystem: "pip" 8 | # Look for `package.json` and `lock` files in the `root` directory 9 | directory: "/" 10 | # Check the npm registry for updates every day (weekdays) 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | 18 | - name: Install dependencies 19 | run: python -m pip install poetry 20 | 21 | - name: Build and publish 22 | env: 23 | POETRY_USERNAME: ${{ secrets.PYPI_USERNAME }} 24 | POETRY_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 25 | run: | 26 | poetry build 27 | poetry publish -u $POETRY_USERNAME -p $POETRY_PASSWORD 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python-version }} on ${{ matrix.platform }} 12 | runs-on: ${{ matrix.platform }} 13 | env: 14 | USING_COVERAGE: "3.10" 15 | 16 | strategy: 17 | matrix: 18 | platform: [ubuntu-latest, windows-latest] 19 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install dependencies 28 | run: python -m pip install tox-gh-actions poetry 29 | 30 | - name: Run tox 31 | run: poetry run tox 32 | 33 | - name: Upload coverage to Codecov 34 | if: contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest' 35 | uses: codecov/codecov-action@v4 36 | with: 37 | fail_ci_if_error: true 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | verbose: true 40 | 41 | package: 42 | name: Build package 43 | runs-on: ${{ matrix.os }} 44 | 45 | strategy: 46 | matrix: 47 | os: [ubuntu-latest, windows-latest, macos-latest] 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: actions/setup-python@v4 52 | with: 53 | python-version: "3.10" 54 | 55 | - name: Install poetry 56 | run: python -m pip install poetry 57 | 58 | - name: Build package 59 | run: poetry build 60 | 61 | - name: Show result 62 | run: ls -l dist 63 | 64 | - name: Install package 65 | run: python -m pip install . 66 | 67 | - name: Import package 68 | run: python -c "import cwt; print(cwt.__version__)" 69 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '25 7 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 25.1.0 4 | hooks: 5 | - id: black 6 | args: [--line-length, "128"] 7 | 8 | - repo: https://github.com/asottile/blacken-docs 9 | rev: 1.19.1 10 | hooks: 11 | - id: blacken-docs 12 | 13 | - repo: https://github.com/PyCQA/flake8 14 | rev: 7.2.0 15 | hooks: 16 | - id: flake8 17 | args: [--ignore, "E203,E501,B006,W503"] 18 | additional_dependencies: [flake8-bugbear] 19 | 20 | - repo: https://github.com/PyCQA/isort 21 | rev: 6.0.1 22 | hooks: 23 | - id: isort 24 | args: [--profile, "black"] 25 | 26 | - repo: https://github.com/pre-commit/mirrors-mypy 27 | rev: v1.15.0 28 | hooks: 29 | - id: mypy 30 | args: [--ignore-missing-imports] 31 | additional_dependencies: [types-requests==2.26.3] 32 | 33 | - repo: https://github.com/pre-commit/pre-commit-hooks 34 | rev: v5.0.0 35 | hooks: 36 | - id: check-json 37 | - id: check-toml 38 | - id: check-yaml 39 | - id: debug-statements 40 | - id: end-of-file-fixer 41 | - id: fix-byte-order-marker 42 | - id: trailing-whitespace 43 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | python: 9 | install: 10 | - method: pip 11 | path: . 12 | extra_requirements: 13 | - docs 14 | 15 | sphinx: 16 | configuration: docs/conf.py 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ajitomi@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] 44 | 45 | [homepage]: https://www.contributor-covenant.org/ 46 | [version]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Ajitomi Daisuke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 3.0.x | :white_check_mark: | 8 | | < 3 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Please use maintainer's email: dajiaji@gmail.com 13 | 14 | DO NOT REPORT SECURITY VULNERABILITIES VIA THE ISSUES OF THIS REPOSITORY. 15 | -------------------------------------------------------------------------------- /cwt/__init__.py: -------------------------------------------------------------------------------- 1 | from .claims import Claims 2 | from .cose import COSE 3 | from .cose_key import COSEKey 4 | from .cose_message import COSEMessage 5 | from .cwt import ( 6 | CWT, 7 | decode, 8 | encode, 9 | encode_and_encrypt, 10 | encode_and_mac, 11 | encode_and_sign, 12 | set_private_claim_names, 13 | ) 14 | from .encrypted_cose_key import EncryptedCOSEKey 15 | from .enums import ( 16 | COSEAlgs, 17 | COSEHeaders, 18 | COSEKeyCrvs, 19 | COSEKeyOps, 20 | COSEKeyParams, 21 | COSEKeyTypes, 22 | COSETypes, 23 | CWTClaims, 24 | ) 25 | from .exceptions import CWTError, DecodeError, EncodeError, VerifyError 26 | from .helpers.hcert import load_pem_hcert_dsc 27 | from .recipient import Recipient 28 | from .signer import Signer 29 | 30 | __version__ = "3.0.0" 31 | __title__ = "cwt" 32 | __description__ = "A Python implementation of CWT/COSE" 33 | __url__ = "https://python-cwt.readthedocs.io" 34 | __uri__ = __url__ 35 | __doc__ = __description__ + " <" + __uri__ + ">" 36 | __author__ = "Ajitomi Daisuke" 37 | __email__ = "ajitomi@gmail.com" 38 | __license__ = "MIT" 39 | __copyright__ = "Copyright 2021-2022 Ajitomi Daisuke" 40 | __all__ = [ 41 | "encode", 42 | "encode_and_mac", 43 | "encode_and_sign", 44 | "encode_and_encrypt", 45 | "decode", 46 | "set_private_claim_names", 47 | "COSE", 48 | "COSEAlgs", 49 | "COSEHeaders", 50 | "COSEKeyCrvs", 51 | "COSEKeyOps", 52 | "COSEKeyParams", 53 | "COSEKeyTypes", 54 | "COSETypes", 55 | "COSEKey", 56 | "COSEMessage", 57 | "COSESignature", 58 | "CWT", 59 | "CWTClaims", 60 | "EncryptedCOSEKey", 61 | "HPKECipherSuite", 62 | "Claims", 63 | "Recipient", 64 | "Signer", 65 | "load_pem_hcert_dsc", 66 | "CWTError", 67 | "EncodeError", 68 | "DecodeError", 69 | "VerifyError", 70 | ] 71 | -------------------------------------------------------------------------------- /cwt/algs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dajiaji/python-cwt/2d5f5c6bda76545c0f2d615424811c63162b09c3/cwt/algs/__init__.py -------------------------------------------------------------------------------- /cwt/algs/asymmetric.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from cryptography.x509 import Certificate, DNSName, load_der_x509_certificate 4 | from cryptography.x509.oid import NameOID 5 | from cryptography.x509.verification import PolicyBuilder, Store 6 | 7 | from ..cose_key_interface import COSEKeyInterface 8 | from ..exceptions import VerifyError 9 | 10 | 11 | class AsymmetricKey(COSEKeyInterface): 12 | def __init__(self, params: Dict[int, Any]): 13 | super().__init__(params) 14 | 15 | self._key: Any = b"" 16 | self._cert: Certificate = None 17 | self._intermediates: List[Certificate] = [] 18 | 19 | if 33 in params: 20 | if not isinstance(params[33], (bytes, list)): 21 | raise ValueError("x5c(33) should be bytes(bstr) or list.") 22 | certs = [params[33]] if isinstance(params[33], bytes) else params[33] 23 | self._cert = load_der_x509_certificate(certs[0]) 24 | if len(certs) > 1: 25 | for c in certs[1:]: 26 | self._intermediates.append(load_der_x509_certificate(c)) 27 | return 28 | 29 | def validate_certificate(self, ca_certs: List[Certificate]) -> bool: 30 | if not ca_certs: 31 | raise ValueError("ca_certs should be set.") 32 | if not self._cert: 33 | return False 34 | 35 | store = Store(ca_certs) 36 | builder = PolicyBuilder().store(store) 37 | verifier = builder.build_server_verifier( 38 | DNSName(self._cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value) 39 | ) 40 | try: 41 | verifier.verify(self._cert, self._intermediates) 42 | except Exception as err: 43 | raise VerifyError("Failed to validate the certificate bound to the key.") from err 44 | return True 45 | -------------------------------------------------------------------------------- /cwt/algs/non_aead.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 4 | 5 | 6 | class AESCTR: 7 | _MAX_SIZE = 2**31 - 1 8 | 9 | def __init__(self, key: bytes): 10 | if len(key) not in (16, 24, 32): 11 | raise ValueError("AESCTR key must be 128, 192, or 256 bits.") 12 | 13 | self._key = key 14 | 15 | @classmethod 16 | def generate_key(cls, bit_length: int) -> bytes: 17 | if not isinstance(bit_length, int): 18 | raise TypeError("bit_length must be an integer") 19 | 20 | if bit_length not in (128, 192, 256): 21 | raise ValueError("bit_length must be 128, 192, or 256") 22 | 23 | return os.urandom(bit_length // 8) 24 | 25 | def encrypt( 26 | self, 27 | nonce: bytes, 28 | data: bytes, 29 | ) -> bytes: 30 | encryptor = Cipher( 31 | algorithms.AES(self._key), 32 | modes.CTR(nonce), 33 | ).encryptor() 34 | 35 | return encryptor.update(data) + encryptor.finalize() 36 | 37 | def decrypt( 38 | self, 39 | nonce: bytes, 40 | data: bytes, 41 | ) -> bytes: 42 | decryptor = Cipher( 43 | algorithms.AES(self._key), 44 | modes.CTR(nonce), 45 | ).decryptor() 46 | 47 | return decryptor.update(data) + decryptor.finalize() 48 | 49 | 50 | class AESCBC: 51 | _MAX_SIZE = 2**31 - 1 52 | 53 | def __init__(self, key: bytes): 54 | if len(key) not in (16, 24, 32): 55 | raise ValueError("AESCBC key must be 128, 192, or 256 bits.") 56 | 57 | self._key = key 58 | 59 | @classmethod 60 | def generate_key(cls, bit_length: int) -> bytes: 61 | if not isinstance(bit_length, int): 62 | raise TypeError("bit_length must be an integer") 63 | 64 | if bit_length not in (128, 192, 256): 65 | raise ValueError("bit_length must be 128, 192, or 256") 66 | 67 | return os.urandom(bit_length // 8) 68 | 69 | def encrypt( 70 | self, 71 | nonce: bytes, 72 | data: bytes, 73 | ) -> bytes: 74 | encryptor = Cipher( 75 | algorithms.AES(self._key), 76 | modes.CBC(nonce), 77 | ).encryptor() 78 | 79 | return encryptor.update(data) + encryptor.finalize() 80 | 81 | def decrypt( 82 | self, 83 | nonce: bytes, 84 | data: bytes, 85 | ) -> bytes: 86 | decryptor = Cipher( 87 | algorithms.AES(self._key), 88 | modes.CBC(nonce), 89 | ).decryptor() 90 | 91 | return decryptor.update(data) + decryptor.finalize() 92 | -------------------------------------------------------------------------------- /cwt/algs/raw.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ..cose_key_interface import COSEKeyInterface 4 | 5 | 6 | class RawKey(COSEKeyInterface): 7 | def __init__(self, params: Dict[int, Any]): 8 | super().__init__(params) 9 | 10 | self._key: bytes = b"" 11 | self._alg = None 12 | 13 | # Validate kty. 14 | if params[1] != 4: 15 | raise ValueError("kty(1) should be Symmetric(4).") 16 | 17 | # Validate k. 18 | if -1 not in params: 19 | raise ValueError("k(-1) should be set.") 20 | if not isinstance(params[-1], bytes): 21 | raise ValueError("k(-1) should be bytes(bstr).") 22 | self._key = params[-1] 23 | 24 | @property 25 | def key(self) -> bytes: 26 | return self._key 27 | 28 | def to_bytes(self) -> bytes: 29 | return self._key 30 | 31 | def to_dict(self) -> Dict[int, Any]: 32 | res = super().to_dict() 33 | res[-1] = self._key 34 | return res 35 | -------------------------------------------------------------------------------- /cwt/algs/rsa.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from cryptography.hazmat.primitives import hashes 4 | from cryptography.hazmat.primitives.asymmetric import padding 5 | from cryptography.hazmat.primitives.asymmetric.rsa import ( 6 | RSAPrivateNumbers, 7 | RSAPublicKey, 8 | RSAPublicNumbers, 9 | ) 10 | 11 | from ..const import COSE_ALGORITHMS_RSA, COSE_KEY_OPERATION_VALUES 12 | from ..exceptions import EncodeError, VerifyError 13 | from .asymmetric import AsymmetricKey 14 | 15 | 16 | class RSAKey(AsymmetricKey): 17 | _ACCEPTABLE_PUBLIC_KEY_OPS = [ 18 | COSE_KEY_OPERATION_VALUES["verify"], 19 | ] 20 | 21 | _ACCEPTABLE_PRIVATE_KEY_OPS = [ 22 | COSE_KEY_OPERATION_VALUES["sign"], 23 | COSE_KEY_OPERATION_VALUES["verify"], 24 | ] 25 | 26 | def __init__(self, params: Dict[int, Any]): 27 | super().__init__(params) 28 | 29 | self._key: Any = None 30 | self._hash: Any = None 31 | self._padding: Any = None 32 | salt_len: Any = padding.PSS.MAX_LENGTH 33 | 34 | # Validate kty. 35 | if params[1] != 3: 36 | raise ValueError("kty(1) should be RSA(3).") 37 | 38 | # Validate alg. 39 | if 3 not in params: 40 | raise ValueError("alg(3) not found.") 41 | if params[3] not in COSE_ALGORITHMS_RSA.values(): 42 | raise ValueError(f"Unsupported or unknown alg(3) for RSA: {params[3]}.") 43 | if params[3] == -259 or params[3] == -39: 44 | self._hash = hashes.SHA512 45 | salt_len = 64 46 | elif params[3] == -258 or params[3] == -38: 47 | self._hash = hashes.SHA384 48 | salt_len = 48 49 | elif params[3] == -257 or params[3] == -37: 50 | self._hash = hashes.SHA256 51 | salt_len = 32 52 | else: 53 | raise ValueError(f"Unsupported or unknown alg(3) for RSA: {params[3]}.") 54 | if params[3] in [-37, -38, -39]: 55 | self._padding = padding.PSS(mgf=padding.MGF1(self._hash()), salt_length=salt_len) 56 | else: 57 | self._padding = padding.PKCS1v15() 58 | 59 | # Validate key_ops. 60 | if -3 not in params: # the RSA private exponent d. 61 | if not self._key_ops: 62 | self._key_ops = RSAKey._ACCEPTABLE_PUBLIC_KEY_OPS 63 | else: 64 | prohibited = [ops for ops in self._key_ops if ops not in RSAKey._ACCEPTABLE_PUBLIC_KEY_OPS] 65 | if prohibited: 66 | raise ValueError(f"Unknown or not permissible key_ops(4) for RSAKey: {prohibited[0]}.") 67 | else: 68 | if not self._key_ops: 69 | self._key_ops = RSAKey._ACCEPTABLE_PRIVATE_KEY_OPS 70 | else: 71 | prohibited = [ops for ops in self._key_ops if ops not in RSAKey._ACCEPTABLE_PRIVATE_KEY_OPS] 72 | if prohibited: 73 | raise ValueError(f"Unknown or not permissible key_ops(4) for RSAKey: {prohibited[0]}.") 74 | 75 | # Validate RSA specific parameters. 76 | if -1 not in params or not isinstance(params[-1], bytes): 77 | raise ValueError("n(-1) should be set as bytes.") 78 | if -2 not in params or not isinstance(params[-2], bytes): 79 | raise ValueError("e(-2) should be set as bytes.") 80 | 81 | public_numbers = RSAPublicNumbers( 82 | n=int.from_bytes(params[-1], "big"), 83 | e=int.from_bytes(params[-2], "big"), 84 | ) 85 | self._dict = params 86 | if -3 not in params: # the RSA private exponent d. 87 | private_props = [p for p in params.keys() if p in [-4, -5, -6, -7, -8]] 88 | if private_props: 89 | raise ValueError(f"RSA public key should not have private parameter: {private_props[0]}.") 90 | self._key = public_numbers.public_key() 91 | return 92 | 93 | if -3 not in params or not isinstance(params[-3], bytes): 94 | raise ValueError("d(-3) should be set as bytes.") 95 | if -4 not in params or not isinstance(params[-4], bytes): 96 | raise ValueError("p(-4) should be set as bytes.") 97 | if -5 not in params or not isinstance(params[-5], bytes): 98 | raise ValueError("q(-5) should be set as bytes.") 99 | if -6 not in params or not isinstance(params[-6], bytes): 100 | raise ValueError("dP(-6) should be set as bytes.") 101 | if -7 not in params or not isinstance(params[-7], bytes): 102 | raise ValueError("dQ(-7) should be set as bytes.") 103 | if -8 not in params or not isinstance(params[-8], bytes): 104 | raise ValueError("qInv(-8) should be set as bytes.") 105 | 106 | private_numbers = RSAPrivateNumbers( 107 | d=int.from_bytes(params[-3], "big"), 108 | p=int.from_bytes(params[-4], "big"), 109 | q=int.from_bytes(params[-5], "big"), 110 | dmp1=int.from_bytes(params[-6], "big"), 111 | dmq1=int.from_bytes(params[-7], "big"), 112 | iqmp=int.from_bytes(params[-8], "big"), 113 | public_numbers=public_numbers, 114 | ) 115 | self._key = private_numbers.private_key() 116 | return 117 | 118 | def to_dict(self) -> Dict[int, Any]: 119 | return self._dict 120 | 121 | def sign(self, msg: bytes) -> bytes: 122 | if isinstance(self._key, RSAPublicKey): 123 | raise ValueError("Public key cannot be used for signing.") 124 | try: 125 | return self._key.sign(msg, self._padding, self._hash()) 126 | except Exception as err: 127 | raise EncodeError("Failed to sign.") from err 128 | 129 | def verify(self, msg: bytes, sig: bytes): 130 | try: 131 | if isinstance(self._key, RSAPublicKey): 132 | self._key.verify(sig, msg, self._padding, self._hash()) 133 | else: 134 | self._key.public_key().verify(sig, msg, self._padding, self._hash()) 135 | except Exception as err: 136 | raise VerifyError("Failed to verify.") from err 137 | -------------------------------------------------------------------------------- /cwt/cbor_processor.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from cbor2 import dumps, loads 4 | 5 | from .exceptions import DecodeError, EncodeError 6 | 7 | 8 | class CBORProcessor: 9 | def _dumps(self, obj: Any) -> bytes: 10 | try: 11 | return dumps(obj) 12 | except Exception as err: 13 | raise EncodeError("Failed to encode.") from err 14 | 15 | def _loads(self, s: bytes) -> Dict[int, Any]: 16 | try: 17 | return loads(s) 18 | except Exception as err: 19 | raise DecodeError("Failed to decode.") from err 20 | -------------------------------------------------------------------------------- /cwt/claims.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict, List, Union 3 | 4 | from .const import CWT_CLAIM_NAMES 5 | from .cose_key import COSEKey 6 | 7 | 8 | class Claims: 9 | """ 10 | A class for handling CWT Claims like JWT claims. 11 | """ 12 | 13 | def __init__( 14 | self, 15 | claims: Dict[int, Any], 16 | claim_names: Dict[str, int] = CWT_CLAIM_NAMES, 17 | ): 18 | if -260 in claims and not isinstance(claims[-260], dict): 19 | raise ValueError("hcert(-260) should be map.") 20 | if -259 in claims and not isinstance(claims[-259], bytes): 21 | raise ValueError("EUPHNonce(-259) should be bstr.") 22 | if -258 in claims and not isinstance(claims[-258], bytes): 23 | raise ValueError("EATMAROEPrefix(-258) should be bstr.") 24 | if -257 in claims and not isinstance(claims[-257], list): 25 | raise ValueError("EAT-FDO(-257) should be array.") 26 | if 1 in claims and not isinstance(claims[1], str): 27 | raise ValueError("iss(1) should be str.") 28 | if 2 in claims and not isinstance(claims[2], str): 29 | raise ValueError("sub(2) should be str.") 30 | if 3 in claims: 31 | if not isinstance(claims[3], str) and not isinstance(claims[3], list): 32 | raise ValueError("aud(3) should be str or list[str].") 33 | if isinstance(claims[3], list): 34 | for c in claims[3]: 35 | if not isinstance(c, str): 36 | raise ValueError("aud(3) should be str or list[str].") 37 | if 4 in claims and not (isinstance(claims[4], int) or isinstance(claims[4], float)): 38 | raise ValueError("exp(4) should be int or float.") 39 | if 5 in claims and not (isinstance(claims[5], int) or isinstance(claims[5], float)): 40 | raise ValueError("nbf(5) should be int or float.") 41 | if 6 in claims and not (isinstance(claims[6], int) or isinstance(claims[6], float)): 42 | raise ValueError("iat(6) should be int or float.") 43 | if 7 in claims and not isinstance(claims[7], bytes): 44 | raise ValueError("cti(7) should be bytes.") 45 | if 8 in claims: 46 | if not isinstance(claims[8], dict): 47 | raise ValueError("cnf(8) should be dict.") 48 | if 1 in claims[8]: 49 | if not isinstance(claims[8][1], dict): 50 | raise ValueError("COSE_Key in cnf(8) should be dict.") 51 | elif 2 in claims[8]: 52 | if not isinstance(claims[8][2], list): 53 | raise ValueError("Encrypted_COSE_Key in cnf(8) should be list.") 54 | elif 3 in claims[8]: 55 | if not isinstance(claims[8][3], bytes): 56 | raise ValueError("kid in cnf(8) should be bytes.") 57 | else: 58 | raise ValueError("cnf(8) should include COSE_Key, Encrypted_COSE_Key, or kid.") 59 | self._claims = claims 60 | self._claim_names = claim_names 61 | return 62 | 63 | @classmethod 64 | def new(cls, claims: Dict[int, Any], private_claim_names: Dict[str, int] = {}): 65 | """ 66 | Creates a Claims object from a CBOR-like(Dict[int, Any]) claim object. 67 | 68 | Args: 69 | claims (Dict[str, Any]): A CBOR-like(Dict[int, Any]) claim object. 70 | private_claim_names (Dict[str, int]): A set of private claim definitions which 71 | consist of a readable claim name(str) and a claim key(int). 72 | The claim key should be less than -65536 but you can use the 73 | numbers other than pre-registered numbers listed in 74 | `IANA Registry `_. 75 | 76 | Returns: 77 | Claims: A CWT claims object. 78 | 79 | Raises: 80 | ValueError: Invalid arguments. 81 | """ 82 | for v in private_claim_names.values(): 83 | if v in CWT_CLAIM_NAMES.values(): 84 | raise ValueError( 85 | "The claim key should be other than the values listed in https://python-cwt.readthedocs.io/en/stable/claims.html." 86 | ) 87 | claim_names = dict(CWT_CLAIM_NAMES, **private_claim_names) 88 | return cls(claims, claim_names) 89 | 90 | @classmethod 91 | def from_json( 92 | cls, 93 | claims: Union[str, bytes, Dict[str, Any]], 94 | private_claim_names: Dict[str, int] = {}, 95 | ): 96 | """ 97 | Converts a JWT claims object into a CWT claims object which has numeric 98 | keys. If a key string in JSON data cannot be mapped to a numeric key, 99 | it will be skipped. 100 | 101 | Args: 102 | claims (Union[str, bytes, Dict[str, Any]]): A JWT claims object 103 | to be converted. 104 | private_claim_names (Dict[str, int]): A set of private claim definitions which 105 | consist of a readable claim name(str) and a claim key(int). 106 | The claim key should be less than -65536 but you can use the 107 | numbers other than pre-registered numbers listed in 108 | `IANA Registry `_. 109 | 110 | Returns: 111 | Claims: A CWT claims object. 112 | 113 | Raises: 114 | ValueError: Invalid arguments. 115 | """ 116 | json_claims: Dict[str, Any] = {} 117 | if isinstance(claims, str) or isinstance(claims, bytes): 118 | json_claims = json.loads(claims) 119 | else: 120 | json_claims = claims 121 | 122 | for k in json_claims: 123 | if not isinstance(k, int): 124 | break 125 | raise ValueError("It is already CBOR-like format.") 126 | 127 | # Convert JSON to CBOR (Convert the type of key from str to int). 128 | cbor_claims: Dict[int, Any] = {} 129 | for k, v in json_claims.items(): 130 | if k not in CWT_CLAIM_NAMES: 131 | if k in private_claim_names: 132 | cbor_claims[private_claim_names[k]] = v 133 | elif k == "cnf": 134 | if not isinstance(v, dict): 135 | raise ValueError("cnf value should be dict.") 136 | if "jwk" in v: 137 | key = COSEKey.from_jwk(v["jwk"]) 138 | cbor_claims[CWT_CLAIM_NAMES[k]] = {1: key.to_dict()} 139 | elif "eck" in v: 140 | cbor_claims[CWT_CLAIM_NAMES[k]] = {2: v["eck"]} 141 | elif "kid" in v: 142 | cbor_claims[CWT_CLAIM_NAMES[k]] = {3: v["kid"].encode("utf-8")} 143 | else: 144 | raise ValueError("Supported cnf value not found.") 145 | else: 146 | cbor_claims[CWT_CLAIM_NAMES[k]] = v 147 | 148 | # Convert test string should be bstr into bstr. 149 | # -259: EUPHNonce 150 | # -258: EATMAROEPrefix 151 | # 7: cti 152 | for i in [-259, -258, 7]: 153 | if i in cbor_claims and isinstance(cbor_claims[i], str): 154 | cbor_claims[i] = cbor_claims[i].encode("utf-8") 155 | return cls.new(cbor_claims, private_claim_names) 156 | 157 | @classmethod 158 | def validate(cls, claims: Dict[int, Any]): 159 | """ 160 | Validates a CWT claims object. 161 | 162 | Args: 163 | claims (Dict[int, Any]): A CWT claims object to be validated. 164 | 165 | Raises: 166 | ValueError: Failed to verify. 167 | """ 168 | cls(claims) 169 | return 170 | 171 | @property 172 | def iss(self) -> Union[str, None]: 173 | return self._claims.get(1, None) 174 | 175 | @property 176 | def sub(self) -> Union[str, None]: 177 | return self._claims.get(2, None) 178 | 179 | @property 180 | def aud(self) -> Union[str, None]: 181 | return self._claims.get(3, None) 182 | 183 | @property 184 | def exp(self) -> Union[int, None]: 185 | return self._claims.get(4, None) 186 | 187 | @property 188 | def nbf(self) -> Union[int, None]: 189 | return self._claims.get(5, None) 190 | 191 | @property 192 | def iat(self) -> Union[int, None]: 193 | return self._claims.get(6, None) 194 | 195 | @property 196 | def cti(self) -> Union[str, None]: 197 | if 7 not in self._claims: 198 | return None 199 | return self._claims[7].decode("utf-8") 200 | 201 | @property 202 | def hcert(self) -> Union[dict, None]: 203 | return self._claims.get(-260, None) 204 | 205 | @property 206 | def cnf(self) -> Union[Dict[int, Any], List[Any], str, None]: 207 | if 8 not in self._claims: 208 | return None 209 | if 1 in self._claims[8]: 210 | key: Dict[int, Any] = self._claims[8][1] 211 | return key 212 | if 2 in self._claims[8]: 213 | eck: List[Any] = self._claims[8][2] 214 | return eck 215 | kid: bytes = self._claims[8][3] 216 | return kid.decode("utf-8") 217 | 218 | def get(self, key: Union[str, int]) -> Any: 219 | """ 220 | Gets a claim value with a claim key. 221 | 222 | Args: 223 | key (Union[str, int]): A claim key. 224 | Returns: 225 | Any: The value of the claim. 226 | """ 227 | int_key = 0 228 | if isinstance(key, str): 229 | int_key = self._claim_names.get(key, 0) 230 | else: 231 | int_key = key 232 | return self._claims.get(int_key, None) if int_key != 0 else None 233 | 234 | def to_dict(self) -> Dict[int, Any]: 235 | """ 236 | Returns a raw claim object. 237 | 238 | Returns: 239 | Any: The value of the raw claim. 240 | """ 241 | return self._claims 242 | -------------------------------------------------------------------------------- /cwt/encrypted_cose_key.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Union 2 | 3 | import cbor2 4 | from cbor2 import CBORTag 5 | 6 | from .cbor_processor import CBORProcessor 7 | from .cose import COSE 8 | from .cose_key import COSEKey 9 | from .cose_key_interface import COSEKeyInterface 10 | 11 | 12 | class EncryptedCOSEKey(CBORProcessor): 13 | """ 14 | An encrypted COSE key. 15 | """ 16 | 17 | @staticmethod 18 | def from_cose_key( 19 | key: COSEKeyInterface, 20 | encryption_key: COSEKeyInterface, 21 | nonce: bytes = b"", 22 | tagged: bool = False, 23 | ) -> Union[List[Any], bytes]: 24 | """ 25 | Returns an encrypted COSE key formatted to COSE_Encrypt0 structure. 26 | 27 | Args: 28 | key: COSEKeyInterface: A key to be encrypted. 29 | encryption_key: COSEKeyInterface: An encryption key to encrypt the 30 | target COSE key. 31 | nonce (bytes): A nonce for encryption. 32 | tagged (bool): An indicator whether the response is wrapped by CWT 33 | tag(61) or not. 34 | Returns: 35 | Union[List[Any], bytes]: A COSE_Encrypt0 structure of the target COSE key. 36 | Raises: 37 | ValueError: Invalid arguments. 38 | EncodeError: Failed to encrypt the COSE key. 39 | """ 40 | protected: Dict[int, Any] = {1: encryption_key.alg} 41 | unprotected: Dict[int, Any] = {4: encryption_key.kid} if encryption_key.kid else {} 42 | if not nonce: 43 | try: 44 | nonce = encryption_key.generate_nonce() 45 | except NotImplementedError: 46 | raise ValueError("Nonce generation is not supported for the key. Set a nonce explicitly.") 47 | unprotected[5] = nonce 48 | b_payload = cbor2.dumps(key.to_dict()) 49 | res: CBORTag = COSE().encode_and_encrypt( 50 | b_payload, 51 | encryption_key, 52 | protected, 53 | unprotected, 54 | out="cbor2/CBORTag", 55 | ) 56 | return res.value 57 | 58 | @staticmethod 59 | def to_cose_key(key: List[Any], encryption_key: COSEKeyInterface) -> COSEKeyInterface: 60 | """ 61 | Returns an decrypted COSE key. 62 | 63 | Args: 64 | key: COSEKeyInterface: A key formatted to COSE_Encrypt0 structure to be decrypted. 65 | encryption_key: COSEKeyInterface: An encryption key to decrypt the target COSE key. 66 | Returns: 67 | COSEKeyInterface: A key decrypted. 68 | Raises: 69 | ValueError: Invalid arguments. 70 | DecodeError: Failed to decode the COSE key. 71 | VerifyError: Failed to verify the COSE key. 72 | """ 73 | res = cbor2.loads(COSE().decode(CBORTag(16, key), encryption_key)) 74 | return COSEKey.new(res) 75 | -------------------------------------------------------------------------------- /cwt/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class COSETypes(enum.IntEnum): 5 | ENCRYPT0 = 1 6 | ENCRYPT = 2 7 | MAC0 = 3 8 | MAC = 4 9 | SIGN1 = 5 10 | SIGN = 6 11 | COUNTERSIGNATURE = 7 12 | RECIPIENT = 8 13 | SIGNATURE = 9 14 | 15 | 16 | class COSEHeaders(enum.IntEnum): 17 | ALG = 1 18 | CRIT = 2 19 | CTY = 3 20 | KID = 4 21 | IV = 5 22 | PARTIAL_IV = 6 23 | COUNTER_SIGNATURE = 7 24 | COUNTER_SIGNATURE_0 = 9 25 | KID_CONTEXT = 10 26 | COUNTER_SIGNATURE_V2 = 11 27 | COUNTER_SIGNATURE_0_V2 = 12 28 | CWT_CLAIMS = 13 29 | X5BAG = 32 30 | X5CHAIN = 33 31 | X5T = 34 32 | X5U = 35 33 | CUPH_NONCE = 256 34 | CUPH_OWNER_PUB_KEY = 257 35 | 36 | 37 | class COSEKeyParams(enum.IntEnum): 38 | KTY = 1 39 | KID = 2 40 | ALG = 3 41 | KEY_OPS = 4 42 | BASE_IV = 5 43 | CRV = -1 44 | X = -2 45 | Y = -3 46 | D = -4 47 | RSA_N = -1 48 | RSA_E = -2 49 | RSA_D = -3 50 | RSA_P = -4 51 | RSA_Q = -5 52 | RSA_DP = -6 53 | RSA_DQ = -7 54 | RSA_QINV = -8 55 | RSA_OTHER = -9 56 | RSA_R_I = -10 57 | RsA_D_I = -11 58 | RSA_T_I = -12 59 | K = -1 60 | 61 | 62 | class COSEAlgs(enum.IntEnum): 63 | A128CTR = -65534 64 | A192CTR = -65533 65 | A256CTR = -65532 66 | A128CBC = -65531 67 | A192CBC = -65530 68 | A256CBC = -65529 69 | RS512 = -259 70 | RS384 = -258 71 | RS256 = -257 72 | ES256K = -47 73 | PS512 = -39 74 | PS384 = -38 75 | PS256 = -37 76 | ES512 = -36 77 | ES384 = -35 78 | ECDH_SS_A256KW = -34 79 | ECDH_SS_A192KW = -33 80 | ECDH_SS_A128KW = -32 81 | ECDH_ES_A256KW = -31 82 | ECDH_ES_A192KW = -30 83 | ECDH_ES_A128KW = -29 84 | ECDH_SS_HKDF_512 = -28 85 | ECDH_SS_HKDF_256 = -27 86 | ECDH_ES_HKDF_512 = -26 87 | ECDH_ES_HKDF_256 = -25 88 | DIRECT_HKDF_SHA512 = -11 89 | DIRECT_HKDF_SHA256 = -10 90 | EDDSA = -8 91 | ES256 = -7 92 | DIRECT = -6 93 | A256KW = -5 94 | A192KW = -4 95 | A128KW = -3 96 | A128GCM = 1 97 | A192GCM = 2 98 | A256GCM = 3 99 | HS256_64 = 4 100 | HS256 = 5 101 | HS384 = 6 102 | HS512 = 7 103 | AES_CCM_16_64_128 = 10 104 | AES_CCM_16_64_256 = 11 105 | AES_CCM_64_64_128 = 12 106 | AES_CCM_64_64_256 = 13 107 | CHACHA20_POLY1305 = 24 108 | AES_CCM_16_128_128 = 30 109 | AES_CCM_16_128_256 = 31 110 | AES_CCM_64_128_128 = 32 111 | AES_CCM_64_128_256 = 33 112 | HPKE_BASE_P256_SHA256_AES128GCM = 35 113 | HPKE_BASE_P256_SHA256_CHACHA20POLY1305 = 36 114 | HPKE_BASE_P384_SHA384_AES256GCM = 37 115 | HPKE_BASE_P384_SHA384_CHACHA20POLY1305 = 38 116 | HPKE_BASE_P521_SHA512_AES256GCM = 39 117 | HPKE_BASE_P521_SHA512_CHACHA20POLY1305 = 40 118 | HPKE_BASE_X25519_SHA256_AES128GCM = 41 119 | HPKE_BASE_X25519_SHA256_CHACHA20POLY1305 = 42 120 | HPKE_BASE_X448_SHA512_AES256GCM = 43 121 | HPKE_BASE_X448_SHA512_CHACHA20POLY1305 = 44 122 | 123 | 124 | class CWTClaims(enum.IntEnum): 125 | HCERT = -260 126 | EUPH_NONCE = -259 127 | EAT_MAROE_PREFIX = -258 128 | EAT_FDO = -257 129 | ISS = 1 130 | SUB = 2 131 | AUD = 3 132 | EXP = 4 133 | NBF = 5 134 | IAT = 6 135 | CTI = 7 136 | CNF = 8 137 | NONCE = 10 138 | UEID = 11 139 | OEMID = 13 140 | SEC_LEVEL = 14 141 | SEC_BOOT = 15 142 | DBG_STAT = 16 143 | LOCATION = 17 144 | EAT_PROFILE = 18 145 | SUBMODS = 20 146 | 147 | 148 | class COSEKeyTypes(enum.IntEnum): 149 | OKP = 1 150 | EC2 = 2 151 | RSA = 3 152 | ASYMMETRIC = 4 153 | # HSS_LMS = 5 154 | # WALNUT_DSA = 6 155 | 156 | 157 | class COSEKeyCrvs(enum.IntEnum): 158 | P256 = 1 159 | P384 = 2 160 | P521 = 3 161 | X25519 = 4 162 | X448 = 5 163 | ED25519 = 6 164 | ED448 = 7 165 | SECP256K1 = 8 166 | 167 | 168 | class COSEKeyOps(enum.IntEnum): 169 | SIGN = 1 170 | VERIFY = 2 171 | ENCRYPT = 3 172 | DECRYPT = 4 173 | WRAP_KEY = 5 174 | UNWRAP_KEY = 6 175 | DERIVE_KEY = 7 176 | DERIVE_BITS = 8 177 | MAC_CREATE = 9 178 | MAC_VERIFY = 10 179 | -------------------------------------------------------------------------------- /cwt/exceptions.py: -------------------------------------------------------------------------------- 1 | class CWTError(Exception): 2 | """ 3 | Base class for all exceptions. 4 | """ 5 | 6 | pass 7 | 8 | 9 | class VerifyError(CWTError): 10 | """ 11 | An Exception occurred when a verification process failed. 12 | """ 13 | 14 | pass 15 | 16 | 17 | class EncodeError(CWTError): 18 | """ 19 | An Exception occurred when a CWT/COSE encoding process failed. 20 | """ 21 | 22 | pass 23 | 24 | 25 | class DecodeError(CWTError): 26 | """ 27 | An Exception occurred when a CWT/COSE decoding process failed. 28 | """ 29 | 30 | pass 31 | -------------------------------------------------------------------------------- /cwt/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dajiaji/python-cwt/2d5f5c6bda76545c0f2d615424811c63162b09c3/cwt/helpers/__init__.py -------------------------------------------------------------------------------- /cwt/helpers/hcert.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Union 2 | 3 | from cryptography import x509 4 | from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey 5 | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey 6 | from cryptography.hazmat.primitives.hashes import SHA256 7 | 8 | from ..algs.ec2 import EC2Key 9 | from ..const import COSE_KEY_TYPES 10 | from ..cose_key import COSEKey 11 | from ..cose_key_interface import COSEKeyInterface 12 | from ..utils import uint_to_bytes 13 | 14 | 15 | def _generate_kid(cert: bytes) -> bytes: 16 | c = x509.load_pem_x509_certificate(cert) 17 | fp = c.fingerprint(SHA256()) 18 | return fp[0:8] 19 | 20 | 21 | def load_pem_hcert_dsc(cert: Union[str, bytes]) -> COSEKeyInterface: 22 | """ 23 | Loads PEM-formatted DSC (Digital Signing Certificate) issued by CSCA 24 | (Certificate Signing Certificate Authority) as a COSEKey. At this time, 25 | the kid of the COSE key will be generated as a 8-byte truncated SHA256 26 | fingerprint of the DSC complient with `Electronic Health Certificate 27 | Specification `_. 28 | 29 | Args: 30 | cert(str): A DSC. 31 | Returns: 32 | COSEKeyInterface: A DSC's public key as a COSE key. 33 | """ 34 | if isinstance(cert, str): 35 | cert = cert.encode("utf-8") 36 | k: Any = None 37 | if b"BEGIN CERTIFICATE" in cert: 38 | k = x509.load_pem_x509_certificate(cert).public_key() 39 | else: 40 | raise ValueError("Invalid PEM data.") 41 | params: Dict[int, Any] = {} 42 | params[2] = _generate_kid(cert) 43 | 44 | if isinstance(k, RSAPublicKey): 45 | alg = -37 # "PS256" 46 | params[1] = COSE_KEY_TYPES["RSA"] 47 | params[3] = alg 48 | pub_nums = k.public_numbers() 49 | params[-1] = uint_to_bytes(pub_nums.n) 50 | params[-2] = uint_to_bytes(pub_nums.e) 51 | elif isinstance(k, EllipticCurvePublicKey): 52 | alg = -7 # "ES256" 53 | params[3] = alg 54 | params.update(EC2Key.to_cose_key(k)) 55 | else: 56 | raise ValueError(f"Unsupported or unknown key type: {type(k)}.") 57 | return COSEKey.new(params) 58 | -------------------------------------------------------------------------------- /cwt/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dajiaji/python-cwt/2d5f5c6bda76545c0f2d615424811c63162b09c3/cwt/py.typed -------------------------------------------------------------------------------- /cwt/recipient.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Union 2 | 3 | import cbor2 4 | 5 | from .const import ( # COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_WITH_KEY_WRAP, 6 | COSE_ALGORITHMS_CKDM, 7 | COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_DIRECT, 8 | COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_WITH_KEY_WRAP, 9 | COSE_ALGORITHMS_HPKE, 10 | COSE_ALGORITHMS_KEY_WRAP, 11 | COSE_ALGORITHMS_RECIPIENT, 12 | ) 13 | from .cose_key import COSEKey 14 | from .cose_key_interface import COSEKeyInterface 15 | from .recipient_algs.aes_key_wrap import AESKeyWrap 16 | from .recipient_algs.direct_hkdf import DirectHKDF 17 | from .recipient_algs.direct_key import DirectKey 18 | from .recipient_algs.ecdh_aes_key_wrap import ECDH_AESKeyWrap 19 | from .recipient_algs.ecdh_direct_hkdf import ECDH_DirectHKDF 20 | from .recipient_algs.hpke import HPKE 21 | from .recipient_interface import RecipientInterface 22 | from .utils import to_cose_header, to_recipient_context 23 | 24 | 25 | class Recipient: 26 | """ 27 | A :class:`RecipientInterface ` Builder. 28 | """ 29 | 30 | @classmethod 31 | def new( 32 | cls, 33 | protected: dict = {}, 34 | unprotected: dict = {}, 35 | ciphertext: bytes = b"", 36 | recipients: List[Any] = [], 37 | sender_key: Optional[COSEKeyInterface] = None, 38 | recipient_key: Optional[COSEKeyInterface] = None, 39 | context: Optional[Union[List[Any], Dict[str, Any]]] = None, 40 | ) -> RecipientInterface: 41 | """ 42 | Creates a recipient from a CBOR-like dictionary with numeric keys. 43 | 44 | Args: 45 | protected (dict): Parameters that are to be cryptographically protected. 46 | unprotected (dict): Parameters that are not cryptographically protected. 47 | ciphertext (List[Any]): A cipher text. 48 | sender_key (Optional[COSEKeyInterface]): A sender private key as COSEKey. 49 | recipient_key (Optional[COSEKeyInterface]): A recipient public key as COSEKey. 50 | context (Optional[Union[List[Any], Dict[str, Any]]]): Context 51 | information structure. 52 | Returns: 53 | RecipientInterface: A recipient object. 54 | Raises: 55 | ValueError: Invalid arguments. 56 | """ 57 | p = to_cose_header(protected, algs=COSE_ALGORITHMS_RECIPIENT) 58 | u = to_cose_header(unprotected, algs=COSE_ALGORITHMS_RECIPIENT) 59 | 60 | if 1 in p and 1 in u: 61 | raise ValueError("alg appear both in protected and unprotected.") 62 | alg = u[1] if 1 in u else p.get(1, 0) 63 | if alg == 0: 64 | raise ValueError("alg should be specified.") 65 | 66 | if alg in COSE_ALGORITHMS_CKDM.values(): # Direct encryption mode. 67 | if len(recipients) > 0: 68 | raise ValueError("Recipients for direct encryption mode don't have recipients.") 69 | if len(ciphertext) > 0: 70 | raise ValueError( 71 | "The ciphertext in the recipients for direct encryption mode must be a zero-length byte string." 72 | ) 73 | 74 | if alg == -6: 75 | return DirectKey(p, u) 76 | if alg in COSE_ALGORITHMS_KEY_WRAP.values(): 77 | if len(protected) > 0: 78 | raise ValueError("The protected header must be a zero-length string in key wrap mode with an AE algorithm.") 79 | if not sender_key: 80 | sender_key = COSEKey.from_symmetric_key(alg=alg) 81 | return AESKeyWrap(u, ciphertext, recipients, sender_key) 82 | if alg in COSE_ALGORITHMS_HPKE.values(): 83 | return HPKE(p, u, ciphertext, recipients, recipient_key) # TODO sender_key 84 | 85 | if context is None: 86 | raise ValueError("context should be set.") 87 | ctx = to_recipient_context(alg, u, context) 88 | 89 | if alg in [-10, -11]: 90 | return DirectHKDF(p, u, ctx) 91 | if alg in COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_DIRECT.values(): 92 | return ECDH_DirectHKDF(p, u, ciphertext, recipients, sender_key, recipient_key, ctx) 93 | if alg in COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_WITH_KEY_WRAP.values(): 94 | return ECDH_AESKeyWrap(p, u, ciphertext, recipients, sender_key, recipient_key, ctx) 95 | raise ValueError(f"Unsupported or unknown alg(1): {alg}.") 96 | 97 | @classmethod 98 | def from_list( 99 | cls, 100 | recipient: List[Any], 101 | context: Optional[Union[List[Any], Dict[str, Any]]] = None, 102 | ) -> RecipientInterface: 103 | """ 104 | Creates a recipient from a raw COSE array data. 105 | 106 | Args: 107 | data (Union[str, bytes, Dict[str, Any]]): JSON-formatted recipient data. 108 | Returns: 109 | RecipientInterface: A recipient object. 110 | Raises: 111 | ValueError: Invalid arguments. 112 | DecodeError: Failed to decode the key data. 113 | """ 114 | if not isinstance(recipient, list) or (len(recipient) != 3 and len(recipient) != 4): 115 | raise ValueError("Invalid recipient format.") 116 | if not isinstance(recipient[0], bytes): 117 | raise ValueError("protected header should be bytes.") 118 | protected = {} if not recipient[0] else cbor2.loads(recipient[0]) 119 | if not isinstance(recipient[1], dict): 120 | raise ValueError("unprotected header should be dict.") 121 | if not isinstance(recipient[2], bytes): 122 | raise ValueError("ciphertext should be bytes.") 123 | if len(recipient) == 3: 124 | rec = cls.new(protected, recipient[1], recipient[2], context=context) 125 | rec._set_b_protected(recipient[0]) 126 | return rec 127 | if not isinstance(recipient[3], list): 128 | raise ValueError("recipients should be list.") 129 | recipients: List[RecipientInterface] = [] 130 | for r in recipient[3]: 131 | recipients.append(cls.from_list(r)) 132 | rec = cls.new(protected, recipient[1], recipient[2], recipients, context=context) 133 | rec._set_b_protected(recipient[0]) 134 | return rec 135 | -------------------------------------------------------------------------------- /cwt/recipient_algs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dajiaji/python-cwt/2d5f5c6bda76545c0f2d615424811c63162b09c3/cwt/recipient_algs/__init__.py -------------------------------------------------------------------------------- /cwt/recipient_algs/aes_key_wrap.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple, Union 2 | 3 | from ..const import COSE_KEY_OPERATION_VALUES 4 | from ..cose_key import COSEKey 5 | from ..cose_key_interface import COSEKeyInterface 6 | from ..exceptions import DecodeError 7 | from ..recipient_interface import RecipientInterface 8 | 9 | 10 | class AESKeyWrap(RecipientInterface): 11 | _ACCEPTABLE_KEY_OPS = [ 12 | COSE_KEY_OPERATION_VALUES["wrapKey"], 13 | COSE_KEY_OPERATION_VALUES["unwrapKey"], 14 | ] 15 | 16 | def __init__( 17 | self, 18 | unprotected: Dict[int, Any], 19 | ciphertext: bytes = b"", 20 | recipients: List[Any] = [], 21 | sender_key: Optional[COSEKeyInterface] = None, 22 | ): 23 | if sender_key is None: 24 | raise ValueError("sender_key should be set.") 25 | if sender_key.alg not in [-3, -4, -5]: 26 | raise ValueError(f"Invalid alg in sender_key: {sender_key.alg}.") 27 | if 1 not in unprotected: 28 | raise ValueError("alg(1) not found in unprotected.") 29 | if unprotected[1] != sender_key.alg: 30 | raise ValueError("alg in unprotected and sender_key's alg do not match.") 31 | super().__init__( 32 | {}, 33 | unprotected, 34 | ciphertext, 35 | recipients, 36 | sender_key.key_ops, 37 | sender_key.key, 38 | ) 39 | self._sender_key: COSEKeyInterface = sender_key 40 | 41 | def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], Optional[COSEKeyInterface]]: 42 | self._ciphertext = self._sender_key.wrap_key(plaintext) 43 | return self.to_list(), None 44 | 45 | def decode( 46 | self, key: COSEKeyInterface, aad: bytes = b"", alg: int = 0, as_cose_key: bool = False 47 | ) -> Union[bytes, COSEKeyInterface]: 48 | try: 49 | unwrapped = key.unwrap_key(self._ciphertext) 50 | except Exception as err: 51 | raise DecodeError("Failed to decode key.") from err 52 | if not as_cose_key: 53 | return unwrapped 54 | if not alg: 55 | raise ValueError("alg should be set.") 56 | return COSEKey.from_symmetric_key(unwrapped, alg=alg, kid=self._kid) 57 | -------------------------------------------------------------------------------- /cwt/recipient_algs/direct.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from ..recipient_interface import RecipientInterface 4 | 5 | 6 | class Direct(RecipientInterface): 7 | def __init__( 8 | self, 9 | protected: Dict[int, Any], 10 | unprotected: Dict[int, Any], 11 | ciphertext: bytes = b"", 12 | recipients: List[Any] = [], 13 | ): 14 | super().__init__(protected, unprotected, ciphertext, recipients) 15 | 16 | if self._alg == 0: 17 | raise ValueError("alg(1) not found.") 18 | return 19 | -------------------------------------------------------------------------------- /cwt/recipient_algs/direct_hkdf.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | from typing import Any, Dict, List, Optional, Tuple, Union 3 | 4 | from cryptography.hazmat.primitives import hashes 5 | from cryptography.hazmat.primitives.kdf.hkdf import HKDF 6 | 7 | from ..const import COSE_KEY_LEN, COSE_KEY_OPERATION_VALUES 8 | from ..cose_key import COSEKey 9 | from ..cose_key_interface import COSEKeyInterface 10 | from ..exceptions import DecodeError, EncodeError, VerifyError 11 | from .direct import Direct 12 | 13 | 14 | class DirectHKDF(Direct): 15 | _ACCEPTABLE_KEY_OPS = [ 16 | COSE_KEY_OPERATION_VALUES["deriveKey"], 17 | COSE_KEY_OPERATION_VALUES["deriveBits"], 18 | ] 19 | 20 | def __init__( 21 | self, 22 | protected: Dict[int, Any] = {}, 23 | unprotected: Dict[int, Any] = {}, 24 | context: List[Any] = [], 25 | ): 26 | super().__init__(protected, unprotected, b"", []) 27 | 28 | self._context = context 29 | 30 | self._salt = None 31 | if -20 in unprotected: 32 | self._salt = unprotected[-20] 33 | 34 | self._hash_alg: Any = None 35 | if self._alg == -10: # direct+HKDF-SHA-256 36 | self._hash_alg = hashes.SHA256() 37 | elif self._alg == -11: # direct+HKDF-SHA-512 38 | self._hash_alg = hashes.SHA512() 39 | else: 40 | raise ValueError(f"Unknown alg(3) for direct key with KDF: {self._alg}.") 41 | 42 | # Generate a salt automatically if both of a salt and a PartyU nonce are not specified. 43 | if not self._salt and not self._context[1][1]: 44 | self._salt = token_bytes(32) if self._alg == -10 else token_bytes(64) 45 | self._unprotected[-20] = self._salt 46 | 47 | # PartyU nonce 48 | if self._context[1][1]: 49 | self._unprotected[-22] = self._context[1][1] 50 | # PartyV nonce 51 | if self._context[2][1]: 52 | self._unprotected[-25] = self._context[2][1] 53 | 54 | def verify_key( 55 | self, 56 | material: bytes, 57 | expected_key: bytes, 58 | ): 59 | try: 60 | hkdf = HKDF( 61 | algorithm=self._hash_alg, 62 | length=COSE_KEY_LEN[self._context[0]] // 8, 63 | salt=self._salt, 64 | info=self._dumps(self._context), 65 | ) 66 | hkdf.verify(material, expected_key) 67 | except Exception as err: 68 | raise VerifyError("Failed to verify key.") from err 69 | return 70 | 71 | def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], Optional[COSEKeyInterface]]: 72 | try: 73 | hkdf = HKDF( 74 | algorithm=self._hash_alg, 75 | length=COSE_KEY_LEN[self._context[0]] // 8, 76 | salt=self._salt, 77 | info=self._dumps(self._context), 78 | ) 79 | derived = hkdf.derive(plaintext) 80 | return self.to_list(), COSEKey.from_symmetric_key(derived, self._context[0], kid=self._kid) 81 | except Exception as err: 82 | raise EncodeError("Failed to derive key.") from err 83 | 84 | def decode( 85 | self, key: COSEKeyInterface, aad: bytes = b"", alg: int = 0, as_cose_key: bool = False 86 | ) -> Union[bytes, COSEKeyInterface]: 87 | try: 88 | hkdf = HKDF( 89 | algorithm=self._hash_alg, 90 | length=COSE_KEY_LEN[self._context[0]] // 8, 91 | salt=self._salt, 92 | info=self._dumps(self._context), 93 | ) 94 | derived = hkdf.derive(key.key) 95 | if not as_cose_key: 96 | return derived 97 | return COSEKey.from_symmetric_key(derived, alg=self._context[0], kid=self._kid) 98 | except Exception as err: 99 | raise DecodeError("Failed to decode.") from err 100 | -------------------------------------------------------------------------------- /cwt/recipient_algs/direct_key.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple, Union 2 | 3 | from ..cose_key_interface import COSEKeyInterface 4 | from .direct import Direct 5 | 6 | 7 | class DirectKey(Direct): 8 | def __init__(self, protected: Dict[int, Any] = {}, unprotected: Dict[int, Any] = {}): 9 | super().__init__(protected, unprotected, b"", []) 10 | 11 | if self._alg != -6: 12 | raise ValueError("alg(1) should be direct(-6).") 13 | return 14 | 15 | def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], Optional[COSEKeyInterface]]: 16 | return self.to_list(), None 17 | 18 | def decode( 19 | self, key: COSEKeyInterface, aad: bytes = b"", alg: int = 0, as_cose_key: bool = False 20 | ) -> Union[bytes, COSEKeyInterface]: 21 | if not as_cose_key: 22 | return b"" 23 | return key 24 | -------------------------------------------------------------------------------- /cwt/recipient_algs/ecdh_aes_key_wrap.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple, Union 2 | 3 | from cryptography.hazmat.primitives.keywrap import aes_key_unwrap, aes_key_wrap 4 | 5 | from ..algs.ec2 import EC2Key 6 | from ..const import COSE_KEY_LEN, COSE_KEY_OPERATION_VALUES 7 | from ..cose_key import COSEKey 8 | from ..cose_key_interface import COSEKeyInterface 9 | from ..exceptions import DecodeError, EncodeError 10 | from ..recipient_interface import RecipientInterface 11 | 12 | 13 | class ECDH_AESKeyWrap(RecipientInterface): 14 | _ACCEPTABLE_KEY_OPS = [ 15 | COSE_KEY_OPERATION_VALUES["deriveKey"], 16 | COSE_KEY_OPERATION_VALUES["deriveBits"], 17 | ] 18 | 19 | def __init__( 20 | self, 21 | protected: Dict[int, Any], 22 | unprotected: Dict[int, Any], 23 | ciphertext: bytes = b"", 24 | recipients: List[Any] = [], 25 | sender_key: Optional[COSEKeyInterface] = None, 26 | recipient_key: Optional[COSEKeyInterface] = None, 27 | context: List[Any] = [], 28 | ): 29 | super().__init__(protected, unprotected, ciphertext, recipients) 30 | self._sender_public_key: Any = None 31 | self._sender_key = sender_key 32 | self._recipient_key = recipient_key 33 | self._context = context 34 | 35 | if self._alg in [-29, -30, -31]: # ECDH-ES 36 | if -1 in self.unprotected: 37 | self._unprotected[-1][3] = self._alg 38 | self._sender_public_key = COSEKey.new(self.unprotected[-1]) 39 | elif self._alg in [-32, -33, -34]: # ECDH-SS 40 | if -2 in self.unprotected: 41 | self._unprotected[-2][3] = self._alg 42 | self._sender_public_key = COSEKey.new(self.unprotected[-2]) 43 | else: 44 | raise ValueError(f"Unknown alg(1) for ECDH with key wrap: {self._alg}.") 45 | 46 | def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], Optional[COSEKeyInterface]]: 47 | if not self._recipient_key: 48 | raise ValueError("recipient_key should be set in advance.") 49 | if not self._context: 50 | raise ValueError("context should be set.") 51 | 52 | if self._alg in [-29, -30, -31]: 53 | # ECDH-ES 54 | self._sender_key = EC2Key({1: 2, -1: self._recipient_key.crv, 3: self._alg}) 55 | else: 56 | # ECDH-SS (alg=-32, -33, -34) 57 | if not self._sender_key: 58 | raise ValueError("sender_key should be set in advance.") 59 | wrapping_bytes = self._sender_key.derive_bytes( 60 | COSE_KEY_LEN[self._context[0]] // 8, 61 | info=self._dumps(self._context), 62 | public_key=self._recipient_key, 63 | ) 64 | wrapping_key = COSEKey.from_symmetric_key(wrapping_bytes, alg=self._context[0]) 65 | # wrapping_key = self._sender_key.derive_key(self._context, public_key=self._recipient_key) 66 | if self._alg in [-29, -30, -31]: 67 | # ECDH-ES 68 | self._unprotected[-1] = self._to_cose_key(self._sender_key.key.public_key()) 69 | else: 70 | # ECDH-SS (alg=-32, -33, -34) 71 | self._unprotected[-2] = self._to_cose_key(self._sender_key.key.public_key()) 72 | try: 73 | self._ciphertext = aes_key_wrap(wrapping_key.key, plaintext) 74 | except Exception as err: 75 | raise EncodeError("Failed to wrap key.") from err 76 | return self.to_list(), None 77 | 78 | def decode( 79 | self, key: COSEKeyInterface, aad: bytes = b"", alg: int = 0, as_cose_key: bool = False 80 | ) -> Union[bytes, COSEKeyInterface]: 81 | if not self._context: 82 | raise ValueError("context should be set.") 83 | if not self._sender_public_key: 84 | raise ValueError("sender_public_key should be set.") 85 | 86 | try: 87 | wrapping_key_bytes = key.derive_bytes( 88 | COSE_KEY_LEN[self._context[0]] // 8, 89 | info=self._dumps(self._context), 90 | public_key=self._sender_public_key, 91 | ) 92 | wrapping_key = COSEKey.from_symmetric_key(wrapping_key_bytes, alg=self._context[0]) 93 | # derived = key.derive_key(self._context, public_key=self._sender_public_key) 94 | derived_bytes = aes_key_unwrap(wrapping_key.key, self._ciphertext) 95 | except Exception as err: 96 | raise DecodeError("Failed to decode key.") from err 97 | if not as_cose_key: 98 | return derived_bytes 99 | if alg == 0: 100 | raise ValueError("alg should be set.") 101 | return COSEKey.from_symmetric_key(derived_bytes, alg=alg, kid=self._kid) 102 | -------------------------------------------------------------------------------- /cwt/recipient_algs/ecdh_direct_hkdf.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | from typing import Any, Dict, List, Optional, Tuple, Union 3 | 4 | from ..algs.ec2 import EC2Key 5 | from ..algs.okp import OKPKey 6 | from ..const import COSE_KEY_LEN, COSE_KEY_OPERATION_VALUES 7 | from ..cose_key import COSEKey 8 | from ..cose_key_interface import COSEKeyInterface 9 | from ..exceptions import DecodeError 10 | from .direct import Direct 11 | 12 | 13 | class ECDH_DirectHKDF(Direct): 14 | _ACCEPTABLE_KEY_OPS = [ 15 | COSE_KEY_OPERATION_VALUES["deriveKey"], 16 | COSE_KEY_OPERATION_VALUES["deriveBits"], 17 | ] 18 | 19 | def __init__( 20 | self, 21 | protected: Dict[int, Any], 22 | unprotected: Dict[int, Any], 23 | ciphertext: bytes = b"", 24 | recipients: List[Any] = [], 25 | sender_key: Optional[COSEKeyInterface] = None, 26 | recipient_key: Optional[COSEKeyInterface] = None, 27 | context: List[Any] = [], 28 | ): 29 | super().__init__(protected, unprotected, ciphertext, recipients) 30 | self._sender_public_key: Any = None 31 | self._sender_key = sender_key 32 | self._recipient_key = recipient_key 33 | self._context = context 34 | 35 | self._salt = None 36 | if -20 in unprotected: 37 | self._salt = unprotected[-20] 38 | 39 | if self._alg in [-25, -26]: # ECDH-ES 40 | if -1 in self.unprotected: 41 | self.unprotected[-1][3] = self._alg 42 | self._sender_public_key = COSEKey.new(self.unprotected[-1]) 43 | elif self._alg in [-27, -28]: # ECDH-SS 44 | if -2 in self.unprotected: 45 | self.unprotected[-2][3] = self._alg 46 | self._sender_public_key = COSEKey.new(self.unprotected[-2]) 47 | else: 48 | raise ValueError(f"Unknown alg(1) for ECDH with HKDF: {self._alg}.") 49 | 50 | # Generate a salt automatically if both of a salt and a PartyU nonce are not specified. 51 | if self._alg in [-27, -28]: # ECDH-SS 52 | if not self._salt and not self._context[1][1]: 53 | self._salt = token_bytes(32) if self._alg == -27 else token_bytes(64) 54 | self._unprotected[-20] = self._salt 55 | 56 | # PartyU nonce 57 | if self._context[1][1]: 58 | self._unprotected[-22] = self._context[1][1] 59 | # PartyV nonce 60 | if self._context[2][1]: 61 | self._unprotected[-25] = self._context[2][1] 62 | 63 | def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], Optional[COSEKeyInterface]]: 64 | if not self._recipient_key: 65 | raise ValueError("recipient_key should be set in advance.") 66 | 67 | # Derive key. 68 | if self._alg in [-25, -26]: 69 | # ECDH-ES 70 | if self._recipient_key.kty == 2: 71 | self._sender_key = EC2Key({1: 2, -1: self._recipient_key.crv, 3: self._alg}) 72 | else: 73 | # should drop this support. 74 | self._sender_key = OKPKey({1: 1, -1: self._recipient_key.crv, 3: self._alg}) 75 | else: 76 | # ECDH-SS (alg=-27 or -28) 77 | if not self._sender_key: 78 | raise ValueError("sender_key should be set in advance.") 79 | 80 | derived_bytes = self._sender_key.derive_bytes( 81 | COSE_KEY_LEN[self._context[0]] // 8, 82 | info=self._dumps(self._context), 83 | public_key=self._recipient_key, 84 | ) 85 | derived_key = COSEKey.from_symmetric_key(derived_bytes, alg=self._context[0]) 86 | if self._alg in [-25, -26]: 87 | # ECDH-ES 88 | self._unprotected[-1] = self._to_cose_key(self._sender_key.key.public_key()) 89 | else: 90 | # ECDH-SS (alg=-27 or -28) 91 | self._unprotected[-2] = self._to_cose_key(self._sender_key.key.public_key()) 92 | return self.to_list(), derived_key 93 | 94 | def decode( 95 | self, 96 | key: COSEKeyInterface, 97 | aad: bytes = b"", 98 | alg: int = 0, 99 | as_cose_key: bool = False, 100 | ) -> Union[bytes, COSEKeyInterface]: 101 | if not self._sender_public_key: 102 | raise ValueError("sender_public_key should be set.") 103 | try: 104 | derived_bytes = key.derive_bytes( 105 | COSE_KEY_LEN[self._context[0]] // 8, 106 | info=self._dumps(self._context), 107 | public_key=self._sender_public_key, 108 | ) 109 | except Exception as err: 110 | raise DecodeError("Failed to decode.") from err 111 | if not as_cose_key: 112 | return derived_bytes 113 | return COSEKey.from_symmetric_key(derived_bytes, alg=self._context[0], kid=self._kid) 114 | -------------------------------------------------------------------------------- /cwt/recipient_algs/hpke.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple, Union 2 | 3 | from pyhpke import AEADId, CipherSuite, KDFId, KEMId, KEMKey, KEMKeyInterface 4 | 5 | from ..cose_key import COSEKey 6 | from ..cose_key_interface import COSEKeyInterface 7 | from ..enums import COSEAlgs 8 | from ..exceptions import DecodeError, EncodeError 9 | from ..recipient_interface import RecipientInterface 10 | 11 | 12 | def to_hpke_ciphersuites(alg: int) -> Tuple[int, int, int]: 13 | if alg == COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM: 14 | return 16, 1, 1 15 | if alg == COSEAlgs.HPKE_BASE_P256_SHA256_CHACHA20POLY1305: 16 | return 16, 1, 3 17 | if alg == COSEAlgs.HPKE_BASE_P384_SHA384_AES256GCM: 18 | return 17, 2, 2 19 | if alg == COSEAlgs.HPKE_BASE_P384_SHA384_CHACHA20POLY1305: 20 | return 17, 2, 3 21 | if alg == COSEAlgs.HPKE_BASE_P521_SHA512_AES256GCM: 22 | return 18, 3, 2 23 | if alg == COSEAlgs.HPKE_BASE_P521_SHA512_CHACHA20POLY1305: 24 | return 18, 3, 3 25 | if alg == COSEAlgs.HPKE_BASE_X25519_SHA256_AES128GCM: 26 | return 32, 1, 1 27 | if alg == COSEAlgs.HPKE_BASE_X25519_SHA256_CHACHA20POLY1305: 28 | return 32, 1, 3 29 | if alg == COSEAlgs.HPKE_BASE_X448_SHA512_AES256GCM: 30 | return 33, 3, 2 31 | if alg == COSEAlgs.HPKE_BASE_X448_SHA512_CHACHA20POLY1305: 32 | return 33, 3, 3 33 | raise ValueError("alg should be one of the HPKE algorithms.") 34 | 35 | 36 | class HPKE(RecipientInterface): 37 | def __init__( 38 | self, 39 | protected: Dict[int, Any], 40 | unprotected: Dict[int, Any], 41 | ciphertext: bytes = b"", 42 | recipients: List[Any] = [], 43 | recipient_key: Optional[COSEKeyInterface] = None, 44 | ): 45 | super().__init__(protected, unprotected, ciphertext, recipients) 46 | self._recipient_key = recipient_key 47 | kem, kdf, aead = to_hpke_ciphersuites(self._alg) 48 | self._suite = CipherSuite.new(KEMId(kem), KDFId(kdf), AEADId(aead)) 49 | return 50 | 51 | def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], Optional[COSEKeyInterface]]: 52 | if self._recipient_key is None: 53 | raise ValueError("recipient_key should be set in advance.") 54 | self._kem_key = self._to_kem_key(self._recipient_key) 55 | try: 56 | enc, ctx = self._suite.create_sender_context(self._kem_key) 57 | self._unprotected[-4] = enc 58 | self._ciphertext = ctx.seal(plaintext, aad=aad) 59 | except Exception as err: 60 | raise EncodeError("Failed to seal.") from err 61 | return self.to_list(), None 62 | 63 | def decode( 64 | self, 65 | key: COSEKeyInterface, 66 | aad: bytes = b"", 67 | alg: int = 0, 68 | as_cose_key: bool = False, 69 | ) -> Union[bytes, COSEKeyInterface]: 70 | try: 71 | ctx = self._suite.create_recipient_context(self._unprotected[-4], self._to_kem_key(key)) 72 | raw = ctx.open(self._ciphertext, aad=aad) 73 | if not as_cose_key: 74 | return raw 75 | return COSEKey.from_symmetric_key(raw, alg=alg, kid=self._kid) 76 | except Exception as err: 77 | raise DecodeError("Failed to open.") from err 78 | 79 | def _to_kem_key(self, src: COSEKeyInterface) -> KEMKeyInterface: 80 | return KEMKey.from_pyca_cryptography_key(src.key) 81 | -------------------------------------------------------------------------------- /cwt/recipient_interface.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple, Union 2 | 3 | from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey 4 | from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey 5 | from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey 6 | 7 | from .algs.ec2 import EC2Key 8 | from .algs.okp import OKPKey 9 | from .cbor_processor import CBORProcessor 10 | from .cose_key_interface import COSEKeyInterface 11 | 12 | 13 | class RecipientInterface(CBORProcessor): 14 | """ 15 | The interface class for a COSE Recipient. 16 | """ 17 | 18 | def __init__( 19 | self, 20 | protected: Optional[Dict[int, Any]] = None, 21 | unprotected: Optional[Dict[int, Any]] = None, 22 | ciphertext: bytes = b"", 23 | recipients: List[Any] = [], 24 | key_ops: List[int] = [], 25 | key: bytes = b"", 26 | ): 27 | """ 28 | Constructor. 29 | 30 | Args: 31 | protected (Optional[Dict[int, Any]]): Parameters that are to be cryptographically 32 | protected. 33 | unprotected (Optional[Dict[int, Any]]): Parameters that are not cryptographically 34 | protected. 35 | ciphertext: A ciphertext encoded as bytes. 36 | recipients: A list of recipient information structures. 37 | key_ops: A list of operations that the key is to be used for. 38 | key: A body of the key as bytes. 39 | """ 40 | protected = {} if protected is None else protected 41 | unprotected = {} if unprotected is None else unprotected 42 | self._alg = 0 43 | 44 | # kid 45 | self._kid = b"" 46 | if 4 in protected: 47 | if not isinstance(protected[4], bytes): 48 | raise ValueError("protected[4](kid) should be bytes.") 49 | self._kid = protected[4] 50 | elif 4 in unprotected: 51 | if not isinstance(unprotected[4], bytes): 52 | raise ValueError("unprotected[4](kid) should be bytes.") 53 | self._kid = unprotected[4] 54 | 55 | # alg 56 | if 1 in protected: 57 | if not isinstance(protected[1], int): 58 | raise ValueError("protected[1](alg) should be int.") 59 | self._alg = protected[1] 60 | elif 1 in unprotected: 61 | if not isinstance(unprotected[1], int): 62 | raise ValueError("unprotected[1](alg) should be int.") 63 | self._alg = unprotected[1] 64 | if unprotected[1] == -6: # direct 65 | if len(protected) != 0: 66 | raise ValueError("protected header should be empty.") 67 | if len(ciphertext) != 0: 68 | raise ValueError("ciphertext should be zero-length bytes.") 69 | if len(recipients) != 0: 70 | raise ValueError("recipients should be absent.") 71 | 72 | # iv 73 | if 5 in unprotected: 74 | if not isinstance(unprotected[5], bytes): 75 | raise ValueError("unprotected[5](iv) should be bytes.") 76 | 77 | self._b_protected: Optional[bytes] = None 78 | self._protected = protected 79 | self._unprotected = unprotected 80 | self._ciphertext = ciphertext 81 | self._key = key 82 | self._context: List[Any] = [0, [None, None, None], [None, None, None], [None, None]] 83 | 84 | # Validate recipients 85 | self._recipients: List[RecipientInterface] = [] 86 | if not recipients: 87 | return 88 | for recipient in recipients: 89 | if not isinstance(recipient, RecipientInterface): 90 | raise ValueError("Invalid child recipient.") 91 | self._recipients.append(recipient) 92 | return 93 | 94 | @property 95 | def kid(self) -> bytes: 96 | """ 97 | The key identifier. 98 | """ 99 | return self._kid 100 | 101 | @property 102 | def alg(self) -> int: 103 | """ 104 | The algorithm that is used with the key. 105 | """ 106 | return self._alg 107 | 108 | @property 109 | def protected(self) -> Dict[int, Any]: 110 | """ 111 | The parameters that are to be cryptographically protected. 112 | """ 113 | return self._protected 114 | 115 | @property 116 | def b_protected(self) -> bytes: 117 | """ 118 | The binary encoded protected header. 119 | """ 120 | if self._b_protected is None: 121 | return self._dumps(self._protected) 122 | return self._b_protected 123 | 124 | @property 125 | def unprotected(self) -> Dict[int, Any]: 126 | """ 127 | The parameters that are not cryptographically protected. 128 | """ 129 | return self._unprotected 130 | 131 | @property 132 | def ciphertext(self) -> bytes: 133 | """ 134 | The ciphertext encoded as bytes 135 | """ 136 | return self._ciphertext 137 | 138 | @property 139 | def recipients(self) -> List[Any]: 140 | """ 141 | The list of recipient information structures. 142 | """ 143 | return self._recipients 144 | 145 | @property 146 | def context(self) -> List[Any]: 147 | """ 148 | The recipient context information. 149 | """ 150 | return self._context 151 | 152 | def to_list(self) -> List[Any]: 153 | """ 154 | Returns the recipient information as a COSE recipient structure. 155 | 156 | Returns: 157 | List[Any]: The recipient structure. 158 | """ 159 | b_protected = self._dumps(self._protected) if self._protected else b"" 160 | b_ciphertext = self._ciphertext if self._ciphertext else b"" 161 | res: List[Any] = [b_protected, self._unprotected, b_ciphertext] 162 | if not self._recipients: 163 | return res 164 | 165 | children = [] 166 | for recipient in self._recipients: 167 | children.append(recipient.to_list()) 168 | res.append(children) 169 | return res 170 | 171 | def encode( 172 | self, 173 | plaintext: bytes = b"", 174 | aad: bytes = b"", 175 | ) -> Tuple[List[Any], Optional[COSEKeyInterface]]: 176 | """ 177 | Encrypts a specified plaintext to the ciphertext in the COSE_Recipient 178 | structure with the recipient-specific method (e.g., key wrapping, key 179 | agreement, or the combination of them) and sets up the related information 180 | (context information or ciphertext) in the recipient structure. 181 | 182 | This function will be called in COSE.encode_* functions so applications 183 | do not need to call it directly. 184 | 185 | Args: 186 | plaintext (bytes): A plaing text to be encrypted. In most of the cases, 187 | the plaintext is a byte string of a content encryption key. 188 | external_aad (bytes): External additional authenticated data for AEAD. 189 | aad_context (bytes): An additional authenticated data context to build 190 | an Enc_structure internally. 191 | Returns: 192 | Tuple[List[Any], Optional[COSEKeyInterface]]: The encoded COSE_Recipient structure 193 | and a derived key. 194 | Raises: 195 | ValueError: Invalid arguments. 196 | EncodeError: Failed to encode(e.g., wrap, derive) the key. 197 | """ 198 | raise NotImplementedError 199 | 200 | def decode( 201 | self, 202 | key: COSEKeyInterface, 203 | aad: bytes = b"", 204 | alg: int = 0, 205 | as_cose_key: bool = False, 206 | ) -> Union[bytes, COSEKeyInterface]: 207 | """ 208 | Decrypts the ciphertext in the COSE_Recipient structure with the 209 | recipient-specific method (e.g., key wrapping, key agreement, 210 | or the combination of them). 211 | 212 | This function will be called in COSE.decode so applications do not need 213 | to call it directly. 214 | 215 | Args: 216 | key (COSEKeyInterface): The external key to be used for 217 | decrypting the ciphertext in the COSE_Recipient structure. 218 | external_aad (bytes): External additional authenticated data for AEAD. 219 | aad_context (bytes): An additional authenticated data context to build 220 | an Enc_structure internally. 221 | alg (int): The algorithm of the key derived. 222 | as_cose_key (bool): The indicator whether the output will be returned 223 | as a COSEKey or not. 224 | Returns: 225 | Union[bytes, COSEKeyInterface]: The decrypted ciphertext field or The COSEKey 226 | converted from the decrypted ciphertext. 227 | Raises: 228 | ValueError: Invalid arguments. 229 | DecodeError: Failed to decode(e.g., unwrap, derive) the key. 230 | """ 231 | raise NotImplementedError 232 | 233 | def _to_cose_key(self, k: Union[EllipticCurvePublicKey, X25519PublicKey, X448PublicKey]) -> Dict[int, Any]: 234 | if isinstance(k, EllipticCurvePublicKey): 235 | return EC2Key.to_cose_key(k) 236 | return OKPKey.to_cose_key(k) 237 | 238 | def _set_b_protected(self, b_protected: bytes): 239 | self._b_protected = b_protected 240 | return 241 | -------------------------------------------------------------------------------- /cwt/recipients.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Union 2 | 3 | from .cbor_processor import CBORProcessor 4 | from .cose_key import COSEKey 5 | from .cose_key_interface import COSEKeyInterface 6 | from .recipient import Recipient 7 | from .recipient_interface import RecipientInterface 8 | 9 | 10 | class Recipients(CBORProcessor): 11 | """ 12 | A Set of COSE Recipients. 13 | """ 14 | 15 | def __init__(self, recipients: List[RecipientInterface], verify_kid: bool = False): 16 | self._recipients = recipients 17 | self._verify_kid = verify_kid 18 | return 19 | 20 | @classmethod 21 | def from_list( 22 | cls, 23 | recipients: List[Any], 24 | verify_kid: bool = False, 25 | context: Optional[Union[List[Any], Dict[str, Any]]] = None, 26 | ): 27 | """ 28 | Create Recipients from a CBOR-like list. 29 | """ 30 | res: List[RecipientInterface] = [] 31 | for r in recipients: 32 | res.append(Recipient.from_list(r, context)) 33 | return cls(res, verify_kid) 34 | 35 | def derive_key(self, keys: List[COSEKeyInterface], alg: int, external_aad: bytes, content_aad: bytes) -> COSEKeyInterface: 36 | """ 37 | Decodes an appropriate key from recipients or keys provided as a parameter ``keys``. 38 | """ 39 | if not self._recipients: 40 | raise ValueError("No recipients.") 41 | err: Exception = ValueError("key is not found.") 42 | for r in self._recipients: 43 | if not r.kid and self._verify_kid: 44 | raise ValueError("kid should be specified in recipient.") 45 | aad = self._dumps([content_aad, r.b_protected, external_aad]) 46 | if r.kid: 47 | for k in keys: 48 | if k.kid != r.kid: 49 | continue 50 | try: 51 | res = r.decode(k, aad, alg=alg, as_cose_key=True) 52 | if not isinstance(res, COSEKeyInterface): 53 | raise TypeError("Internal type error.") 54 | return res 55 | except Exception as e: 56 | err = e 57 | continue 58 | for k in keys: 59 | try: 60 | res = r.decode(k, aad, alg=alg, as_cose_key=True) 61 | if not isinstance(res, COSEKeyInterface): 62 | raise TypeError("Internal type error.") 63 | return res 64 | except Exception as e: 65 | err = e 66 | raise err 67 | 68 | def _create_key(self, alg: int, k: COSEKeyInterface, r: RecipientInterface) -> COSEKeyInterface: 69 | if r.alg == -6: # direct 70 | # if k.alg != alg: 71 | # raise ValueError("alg mismatch.") 72 | return k 73 | return COSEKey.new({1: 4, 3: alg, -1: r.decode(k)}) 74 | -------------------------------------------------------------------------------- /cwt/signer.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Union 2 | 3 | from .cbor_processor import CBORProcessor 4 | from .const import COSE_ALGORITHMS_SIGNATURE 5 | from .cose_key import COSEKey 6 | from .cose_key_interface import COSEKeyInterface 7 | from .utils import to_cose_header 8 | 9 | 10 | class Signer(CBORProcessor): 11 | """ 12 | A Signer information. 13 | """ 14 | 15 | def __init__( 16 | self, 17 | cose_key: COSEKeyInterface, 18 | protected: Union[Dict[int, Any], bytes], 19 | unprotected: Dict[int, Any], 20 | signature: bytes = b"", 21 | ): 22 | self._cose_key = cose_key 23 | if isinstance(protected, bytes): 24 | self._protected = protected 25 | else: 26 | self._protected = b"" if not protected else self._dumps(protected) 27 | self._unprotected = unprotected 28 | self._signature = signature 29 | return 30 | 31 | @property 32 | def cose_key(self) -> COSEKeyInterface: 33 | """ 34 | The COSE key for the signer. 35 | """ 36 | return self._cose_key 37 | 38 | @property 39 | def protected(self) -> bytes: 40 | """ 41 | The parameters that are to be cryptographically protected. 42 | """ 43 | return self._protected 44 | 45 | @property 46 | def unprotected(self) -> Dict[int, Any]: 47 | """ 48 | The parameters that are not cryptographically protected. 49 | """ 50 | return self._unprotected 51 | 52 | @property 53 | def signature(self) -> bytes: 54 | """ 55 | The signature that the signer signed. 56 | """ 57 | return self._signature 58 | 59 | @classmethod 60 | def new( 61 | cls, 62 | cose_key: COSEKeyInterface, 63 | protected: Union[dict, bytes] = {}, 64 | unprotected: dict = {}, 65 | signature: bytes = b"", 66 | ): 67 | """ 68 | Creates a signer information object (COSE_Signature). 69 | 70 | Args: 71 | cose_key (COSEKey): A signature key for the signer. 72 | protected (Union[dict, bytes]): Parameters that are to be cryptographically 73 | protected. 74 | unprotected (dict): Parameters that are not cryptographically protected. 75 | signature (bytes): A signature as bytes. 76 | Returns: 77 | Signer: A signer information object. 78 | Raises: 79 | ValueError: Invalid arguments. 80 | """ 81 | p: Union[Dict[int, Any], bytes] = ( 82 | to_cose_header(protected, algs=COSE_ALGORITHMS_SIGNATURE) if isinstance(protected, dict) else protected 83 | ) 84 | u = to_cose_header(unprotected, algs=COSE_ALGORITHMS_SIGNATURE) 85 | return cls(cose_key, p, u, signature) 86 | 87 | @classmethod 88 | def from_jwk(cls, data: Union[str, bytes, Dict[str, Any]]): 89 | """ 90 | Creates a signer information object (COSE_Signature) from JWK. 91 | The ``alg`` in the JWK will be included in the protected header, 92 | and the ``kid`` in the JWT will be include in the unprotected header. 93 | If you want to include any other parameters in the protected/unprotected 94 | header, you have to use :func:`Signer.new `. 95 | 96 | Args: 97 | data (Union[str, bytes, Dict[str, Any]]): A JWK. 98 | Returns: 99 | Signer: A signer information object. 100 | Raises: 101 | ValueError: Invalid arguments. 102 | DecodeError: Failed to decode the key data. 103 | """ 104 | protected: Dict[int, Any] = {} 105 | unprotected: Dict[int, Any] = {} 106 | 107 | cose_key = COSEKey.from_jwk(data) 108 | 109 | # alg 110 | if cose_key.alg not in COSE_ALGORITHMS_SIGNATURE.values(): 111 | raise ValueError(f"Unsupported or unknown alg for signature: {cose_key.alg}.") 112 | protected[1] = cose_key.alg 113 | 114 | # kid 115 | if cose_key.kid: 116 | unprotected[4] = cose_key.kid 117 | return cls(cose_key, protected, unprotected) 118 | 119 | @classmethod 120 | def from_pem( 121 | cls, 122 | data: Union[str, bytes], 123 | alg: Union[int, str] = "", 124 | kid: Union[bytes, str] = b"", 125 | ): 126 | """ 127 | Creates a signer information object (COSE_Signature) from PEM-formatted key. 128 | The ``alg`` in the JWK will be included in the protected header, 129 | and the ``kid`` in the JWT will be include in the unprotected header. 130 | If you want to include any other parameters in the protected/unprotected 131 | header, you have to use :func:`Signer.new `. 132 | 133 | Args: 134 | data (Union[str, bytes]): A PEM-formatted key. 135 | alg (Union[int, str]): An algorithm label(int) or name(str). It is only 136 | used when an algorithm cannot be specified by the PEM data, such as 137 | RSA family algorithms. 138 | kid (Union[bytes, str]): A key identifier. 139 | Returns: 140 | Signer: A signer information object. 141 | Raises: 142 | ValueError: Invalid arguments. 143 | DecodeError: Failed to decode the key data. 144 | """ 145 | protected: Dict[int, Any] = {} 146 | unprotected: Dict[int, Any] = {} 147 | 148 | cose_key = COSEKey.from_pem(data, alg=alg, kid=kid) 149 | 150 | # alg 151 | protected[1] = cose_key.alg 152 | 153 | # kid 154 | if cose_key.kid: 155 | unprotected[4] = cose_key.kid 156 | return cls(cose_key, protected, unprotected) 157 | 158 | def sign(self, msg: bytes): 159 | """ 160 | Returns a digital signature for the specified message 161 | using the specified key value. 162 | 163 | Args: 164 | msg (bytes): A message to be signed. 165 | Raises: 166 | ValueError: Invalid arguments. 167 | EncodeError: Failed to sign the message. 168 | """ 169 | self._signature = self._cose_key.sign(msg) 170 | return 171 | 172 | def verify(self, msg: bytes): 173 | """ 174 | Verifies that the specified digital signature is valid 175 | for the specified message. 176 | 177 | Args: 178 | msg (bytes): A message to be verified. 179 | Raises: 180 | ValueError: Invalid arguments. 181 | VerifyError: Failed to verify. 182 | """ 183 | self._cose_key.verify(msg, self._signature) 184 | return 185 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. automodule:: cwt 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :member-order: bysource 9 | 10 | .. automodule:: cwt.cose_key_interface 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | :member-order: bysource 15 | 16 | .. automodule:: cwt.recipient_interface 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | :member-order: bysource 21 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/claims.rst: -------------------------------------------------------------------------------- 1 | Supported CWT Claims 2 | ==================== 3 | 4 | `IANA Registry for CWT Claims`_ lists all of registered CWT claims. 5 | This section shows the claims which this library currently supports. 6 | In particular, class ``CWT`` can validate the type of the claims 7 | and ``Claims.from_json`` can convert the following Names(str) into Values(int). 8 | 9 | CBOR Web Token (CWT) Claims 10 | --------------------------- 11 | 12 | +-----------------+--------+-------+-------------------------------------------------------+ 13 | | Name | Status | Value | Description | 14 | +=================+========+=======+=======================================================+ 15 | | hcert | ✅ | -260 | Health Certificate | 16 | +-----------------+--------+-------+-------------------------------------------------------+ 17 | | EUPHNonce | | -259 | Challenge Nonce defined in FIDO Device Onboarding | 18 | +-----------------+--------+-------+-------------------------------------------------------+ 19 | | EATMAROEPrefix | | -258 | | Signing prefix for multi-app restricted operating | 20 | | | | | | environments | 21 | +-----------------+--------+-------+-------------------------------------------------------+ 22 | | EAT-FDO | | -257 | EAT-FDO may contain related to FIDO Device Onboarding | 23 | +-----------------+--------+-------+-------------------------------------------------------+ 24 | | iss | ✅ | 1 | Issuer | 25 | +-----------------+--------+-------+-------------------------------------------------------+ 26 | | sub | ✅ | 2 | Subject | 27 | +-----------------+--------+-------+-------------------------------------------------------+ 28 | | aud | ✅ | 3 | Audience | 29 | +-----------------+--------+-------+-------------------------------------------------------+ 30 | | exp | ✅ | 4 | Expiration Time | 31 | +-----------------+--------+-------+-------------------------------------------------------+ 32 | | nbf | ✅ | 5 | Not Before | 33 | +-----------------+--------+-------+-------------------------------------------------------+ 34 | | iat | ✅ | 6 | Issued At | 35 | +-----------------+--------+-------+-------------------------------------------------------+ 36 | | cti | ✅ | 7 | CWT ID | 37 | +-----------------+--------+-------+-------------------------------------------------------+ 38 | | cnf | ✅ | 8 | Confirmation | 39 | +-----------------+--------+-------+-------------------------------------------------------+ 40 | | nonce | | 10 | Nonce | 41 | +-----------------+--------+-------+-------------------------------------------------------+ 42 | | ueid | | 11 | Universal Entity ID Claim | 43 | +-----------------+--------+-------+-------------------------------------------------------+ 44 | | oemid | | 13 | OEM Identification by IEEE | 45 | +-----------------+--------+-------+-------------------------------------------------------+ 46 | | seclevel | | 14 | Security Level | 47 | +-----------------+--------+-------+-------------------------------------------------------+ 48 | | secboot | | 15 | Secure Boot | 49 | +-----------------+--------+-------+-------------------------------------------------------+ 50 | | dbgstat | | 16 | Debug Status | 51 | +-----------------+--------+-------+-------------------------------------------------------+ 52 | | location | | 17 | Location | 53 | +-----------------+--------+-------+-------------------------------------------------------+ 54 | | eat_profile | | 18 | EAT Profile | 55 | +-----------------+--------+-------+-------------------------------------------------------+ 56 | | submods | | 20 | The Submodules Part of a Token | 57 | +-----------------+--------+-------+-------------------------------------------------------+ 58 | 59 | CWT Confirmation Methods 60 | ------------------------ 61 | 62 | +--------------------+--------+-------+----------------------------------------------------+ 63 | | Name | Status | Value | Description | 64 | +====================+========+=======+====================================================+ 65 | | COSE_Key | ✅ | 1 | COSE_Key Representing Public Key | 66 | +--------------------+--------+-------+----------------------------------------------------+ 67 | | Encrypted_COSE_Key | ✅ | 2 | Encrypted COSE_Key | 68 | +--------------------+--------+-------+----------------------------------------------------+ 69 | | kid | ✅ | 3 | Key Identifier | 70 | +--------------------+--------+-------+----------------------------------------------------+ 71 | 72 | .. _`IANA Registry for CWT Claims`: https://www.iana.org/assignments/cwt/cwt.xhtml 73 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | from cwt import __version__ as python_cwt_version 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "Python CWT" 22 | copyright = "2021, AJITOMI Daisuke" 23 | author = "AJITOMI Daisuke" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = python_cwt_version 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.viewcode", 37 | "sphinx.ext.todo", 38 | "sphinx.ext.napoleon", 39 | ] 40 | 41 | nitpick_ignore = [ 42 | ("py:class", "T"), 43 | ("py:class", "cwt.COSEKeyInterface"), 44 | ("py:class", "cwt.RecipientInterface"), 45 | ("py:class", "cwt.claims.T"), 46 | ("py:class", "cwt.cbor_processor.CBORProcessor"), 47 | ("py:class", "_cbor2.CBORTag"), 48 | ("py:class", "cwt.exceptions.CWTError"), 49 | ("py:class", "cwt.recipient.Recipient"), 50 | ("py:class", "CBORTag"), 51 | ("py:class", "cwt.claims.Claims"), 52 | ("py:class", "enum.IntEnum"), 53 | ("py:class", "cwt.cose_message.Self"), 54 | ] 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | # templates_path = ['_templates'] 58 | 59 | # List of patterns, relative to source directory, that match files and 60 | # directories to ignore when looking for source files. 61 | # This pattern also affects html_static_path and html_extra_path. 62 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 63 | 64 | 65 | # -- Options for HTML output ------------------------------------------------- 66 | 67 | # The theme to use for HTML and HTML Help pages. See the documentation for 68 | # a list of builtin themes. 69 | # 70 | html_theme = "sphinx_rtd_theme" 71 | 72 | # Add any paths that contain custom static files (such as style sheets) here, 73 | # relative to this directory. They are copied after the builtin static files, 74 | # so a file named "default.css" will overwrite the builtin "default.css". 75 | # html_static_path = ['_static'] 76 | 77 | # -- Extension configuration ------------------------------------------------- 78 | 79 | # Napoleon settings 80 | napoleon_google_docstring = True 81 | napoleon_numpy_docstring = False 82 | napoleon_include_init_with_doc = True 83 | napoleon_include_private_with_doc = False 84 | napoleon_include_special_with_doc = True 85 | napoleon_use_admonition_for_examples = False 86 | napoleon_use_admonition_for_notes = False 87 | napoleon_use_admonition_for_references = False 88 | napoleon_use_ivar = False 89 | napoleon_use_param = True 90 | napoleon_use_rtype = True 91 | 92 | # -- Options for todo extension ---------------------------------------------- 93 | 94 | # If true, `todo` and `todoList` produce output, else they produce nothing. 95 | todo_include_todos = True 96 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Python CWT documentation master file, created by 2 | sphinx-quickstart on Sun Apr 18 02:36:11 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Python CWT 7 | ===================== 8 | 9 | Python CWT is a CBOR Web Token (CWT) and CBOR Object Signing and Encryption (COSE) 10 | implementation compliant with `various COSE related specifications`_. 11 | 12 | You can install Python CWT with pip: 13 | 14 | .. code-block:: console 15 | 16 | $ pip install cwt 17 | 18 | 19 | And then, you can use it as follows: 20 | 21 | COSE API: 22 | 23 | .. code-block:: python 24 | 25 | from cwt import COSE, COSEKey 26 | 27 | mac_key = COSEKey.generate_symmetric_key(alg="HS256", kid="01") 28 | 29 | # The sender side: 30 | sender = COSE.new() 31 | encoded = sender.encode( 32 | b"Hello world!", 33 | mac_key, 34 | protected={"alg": "HS256"}, 35 | unprotected={"kid": "01"}, 36 | ) 37 | 38 | # The recipient side: 39 | recipient = COSE.new() 40 | assert b"Hello world!" == recipient.decode(encoded, mac_key) 41 | 42 | CWT API: 43 | 44 | .. code-block:: python 45 | 46 | import cwt 47 | from cwt import COSEKey 48 | 49 | mac_key = COSEKey.generate_symmetric_key(alg="HS256", kid="01") 50 | 51 | # The sender side: 52 | token = encode({1: "coaps://as.example", 2: "dajiaji", 7: b"123"}, mac_key) 53 | 54 | # The recipient side: 55 | decoded = decode(token, mac_key) 56 | # decoded == {1: 'coaps://as.example', 2: 'dajiaji', 7: b'123', 4: 1620088759, 5: 1620085159, 6: 1620085159} 57 | 58 | 59 | Index 60 | ----- 61 | 62 | .. toctree:: 63 | :maxdepth: 2 64 | 65 | installation 66 | api 67 | claims 68 | algorithms 69 | changes 70 | 71 | .. _`various COSE related specifications`: https://github.com/dajiaji/python-cwt#referenced-specifications 72 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You can install Python CWT with pip: 5 | 6 | .. code-block:: console 7 | 8 | $ pip install cwt 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "cwt" 7 | version = "3.0.0" 8 | description = "A Python implementation of CWT/COSE." 9 | authors = ["Ajitomi Daisuke "] 10 | license = "MIT" 11 | readme = "README.md" 12 | repository = "https://github.com/dajiaji/python-cwt" 13 | 14 | include = [ 15 | "CHANGES.rst", 16 | "docs", 17 | "poetry.lock", 18 | "samples", 19 | "tests", 20 | "tox.ini", 21 | ] 22 | 23 | exclude = [ 24 | "docs/_build", 25 | ] 26 | 27 | [tool.poetry.dependencies] 28 | python = "^3.9.2,<4.0" 29 | cbor2 = "^5.4.2" 30 | cryptography = ">=42.0.1,<45" 31 | pyhpke = ">=0.5.3,<1.0.0" 32 | 33 | [tool.poetry.group.dev] 34 | optional = true 35 | 36 | [tool.poetry.group.dev.dependencies] 37 | pytest = "^8.3" 38 | pytest-cov = "^6.0.0" 39 | tox = "^4.23.2" 40 | pre-commit = "^4.0.1" 41 | 42 | [tool.poetry.group.docs] 43 | optional = true 44 | 45 | [tool.poetry.group.docs.dependencies] 46 | sphinx = ">=7.1,<8" 47 | sphinx-rtd-theme = "^3.0.2" 48 | sphinx-autodoc-typehints = ">=2.3.0,<3.0.0" 49 | docutils = "^0.21.2" 50 | 51 | [tool.mypy] 52 | ignore_missing_imports = true 53 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dajiaji/python-cwt/2d5f5c6bda76545c0f2d615424811c63162b09c3/samples/__init__.py -------------------------------------------------------------------------------- /samples/eudcc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dajiaji/python-cwt/2d5f5c6bda76545c0f2d615424811c63162b09c3/samples/eudcc/__init__.py -------------------------------------------------------------------------------- /samples/eudcc/swedish_verifier.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import zlib 4 | from typing import Any, Dict, Union 5 | 6 | import jwt 7 | import requests 8 | from base45 import b45decode 9 | 10 | import cwt 11 | from cwt import COSEKey 12 | 13 | 14 | class SwedishVerifier: 15 | def __init__(self, base_url: str, trustlist_store_path: str): 16 | self._base_url = base_url 17 | self._trustlist_store_path = trustlist_store_path 18 | self._dscs: list = [] 19 | self._trustlist: list = [] 20 | self._load_trustlist() 21 | 22 | @classmethod 23 | def new(cls, base_url: str, trustlist_store_path: str): 24 | return cls(base_url, trustlist_store_path) 25 | 26 | def refresh_trustlist(self): 27 | self._dscs = [] 28 | self._trustlist = [] 29 | 30 | # Get a trust-list signer certificate. 31 | r = requests.get(self._base_url + "/cert") 32 | if r.status_code != 200: 33 | raise Exception(f"Received {r.status_code} from /cert") 34 | key = r.text 35 | cose_key = COSEKey.from_pem(key) 36 | 37 | # Get DSCs 38 | r = requests.get(self._base_url + "/trust-list") 39 | if r.status_code != 200: 40 | raise Exception(f"Received {r.status_code} from /trust-list") 41 | decoded = jwt.decode( 42 | r.text, 43 | cose_key.key, 44 | algorithms=["ES256"], 45 | options={"verify_aud": False}, 46 | ) 47 | for v in decoded["dsc_trust_list"].values(): 48 | for k in v["keys"]: 49 | if "use" in k and k["use"] == "enc": 50 | # Workaround for Swedish DSC. 51 | del k["use"] 52 | if k["kty"] == "RSA": 53 | k["alg"] = "PS256" 54 | self._dscs.append(COSEKey.from_jwk(k)) 55 | self._trustlist.append(k) 56 | 57 | # Update trustlist store. 58 | with open(self._trustlist_store_path, "w") as f: 59 | json.dump(self._trustlist, f, indent=4) 60 | return 61 | 62 | def verify_and_decode(self, eudcc: bytes) -> Union[Dict[int, Any], bytes]: 63 | if eudcc.startswith(b"HC1:"): 64 | # Decode Base45 data. 65 | eudcc = b45decode(eudcc[4:]) 66 | # Decompress with zlib. 67 | eudcc = zlib.decompress(eudcc) 68 | # Verify and decode CWT. 69 | return cwt.decode(eudcc, keys=self._dscs) 70 | 71 | def _load_trustlist(self): 72 | try: 73 | with open(self._trustlist_store_path) as f: 74 | self._trustlist = json.load(f) 75 | self._dscs = [COSEKey.from_jwk(k) for k in self._trustlist] 76 | except Exception as err: 77 | if type(err) is not FileNotFoundError: 78 | raise err 79 | self._trustlist = [] 80 | return 81 | 82 | 83 | # An endpoint of Digital Green Certificate Verifier Service compliant with: 84 | # https://github.com/DIGGSweden/dgc-trust/blob/main/specifications/trust-list.md 85 | BASE_URL = os.environ["CWT_SAMPLES_EUDCC_BASE_URL"] 86 | 87 | # e.g., "./dscs.json" 88 | TRUSTLIST_STORE_PATH = os.environ["CWT_SAMPLES_EUDCC_TRUSTLIST_STORE_PATH"] 89 | 90 | # quoted from https://github.com/eu-digital-green-certificates/dgc-testdata/blob/main/AT/2DCode/raw/1.json 91 | BASE45_FORMATTED_EUDCC = b"HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH-R6IOOK.IR9B+9G4G50PHZF0AT4V22F/8X*G3M9JUPY0BX/KR96R/S09T./0LWTKD33236J3TA3M*4VV2 73-E3GG396B-43O058YIB73A*G3W19UEBY5:PI0EGSP4*2DN43U*0CEBQ/GXQFY73CIBC:G 7376BXBJBAJ UNFMJCRN0H3PQN*E33H3OA70M3FMJIJN523.K5QZ4A+2XEN QT QTHC31M3+E32R44$28A9H0D3ZCL4JMYAZ+S-A5$XKX6T2YC 35H/ITX8GL2-LH/CJTK96L6SR9MU9RFGJA6Q3QR$P2OIC0JVLA8J3ET3:H3A+2+33U SAAUOT3TPTO4UBZIC0JKQTL*QDKBO.AI9BVYTOCFOPS4IJCOT0$89NT2V457U8+9W2KQ-7LF9-DF07U$B97JJ1D7WKP/HLIJL8JF8JFHJP7NVDEBU1J*Z222E.GJ457661CFFTWM-8P2IUE7K*SSW613:9/:TT5IYQBTBU16R4I1A/9VRPJ-TS.7ZEM7MSVOCD4RG2L-TQJROXL2J:52J7F0Q10SMAP3CG3KHF0DWIH" 92 | # RAW_EUDCC = bytes.fromhex( 93 | # "d2844da20448d919375fc1e7b6b20126a0590133a4041a61817ca0061a60942ea001624154390103a101a4617681aa62646e01626d616d4f52472d3130303033303231356276706a313131393334393030376264746a323032312d30322d313862636f624154626369783155524e3a555643493a30313a41543a31303830373834334639344145453045453530393346424332353442443831332342626d706c45552f312f32302f31353238626973781b4d696e6973747279206f66204865616c74682c20417573747269616273640262746769383430353339303036636e616da463666e74754d5553544552465241553c474f455353494e47455262666e754d7573746572667261752d47c3b6c39f696e67657263676e74684741425249454c4562676e684761627269656c656376657265312e302e3063646f626a313939382d30322d323658405812fce67cb84c3911d78e3f61f890d0c80eb9675806aebed66aa2d0d0c91d1fc98d7bcb80bf00e181806a9502e11b071325901bd0d2c1b6438747b8cc50f521" 94 | # ) 95 | 96 | if __name__ == "__main__": 97 | v = SwedishVerifier.new(BASE_URL, TRUSTLIST_STORE_PATH) 98 | v.refresh_trustlist() 99 | try: 100 | res = v.verify_and_decode(BASE45_FORMATTED_EUDCC) # or RAW_EUDCC 101 | except Exception as err: 102 | print("Verification failed: %s" % err) 103 | exit(1) 104 | hcert = res[-260] 105 | print(hcert) 106 | exit(0) 107 | -------------------------------------------------------------------------------- /samples/eudcc/verifier.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import zlib 4 | from typing import Any, Dict, Union 5 | 6 | import requests 7 | from base45 import b45decode 8 | 9 | import cwt 10 | from cwt import load_pem_hcert_dsc 11 | 12 | 13 | class Verifier: 14 | def __init__(self, base_url: str, trustlist_store_path: str): 15 | self._base_url = base_url 16 | self._trustlist_store_path = trustlist_store_path 17 | self._dscs: list = [] 18 | self._trustlist: list = [] 19 | self._load_trustlist() 20 | 21 | @classmethod 22 | def new(cls, base_url: str, trustlist_store_path: str): 23 | return cls(base_url, trustlist_store_path) 24 | 25 | def refresh_trustlist(self): 26 | status = 200 27 | headers = None 28 | 29 | # Get new DSCs 30 | x_resume_token = self._trustlist[len(self._trustlist) - 1]["x_resume_token"] if self._trustlist else "" 31 | while status == 200: 32 | if x_resume_token: 33 | headers = {"X-RESUME-TOKEN": x_resume_token} 34 | r = requests.get(self._base_url + "/signercertificateUpdate", headers=headers) 35 | status = r.status_code 36 | if status == 204: 37 | break 38 | if status != 200: 39 | raise Exception(f"Received {status} from signercertificateUpdate") 40 | 41 | x_resume_token = r.headers["X-RESUME-TOKEN"] 42 | self._trustlist.append( 43 | { 44 | "x_kid": r.headers["X-KID"], 45 | "x_resume_token": x_resume_token, 46 | "dsc": r.text, 47 | } 48 | ) 49 | 50 | # Filter expired/revoked DSCs 51 | r = requests.get(self._base_url + "/signercertificateStatus") 52 | if r.status_code != 200: 53 | raise Exception(f"Received {r.status_code} from signercertificateStatus") 54 | active_kids = r.json() 55 | self._dscs = [] 56 | for v in self._trustlist: 57 | if v["x_kid"] not in active_kids: 58 | continue 59 | dsc = f"-----BEGIN CERTIFICATE-----\n{v['dsc']}\n-----END CERTIFICATE-----" 60 | self._dscs.append(load_pem_hcert_dsc(dsc)) 61 | 62 | # Update trustlist store. 63 | with open(self._trustlist_store_path, "w") as f: 64 | json.dump([v for v in self._trustlist if v["x_kid"] in active_kids], f, indent=4) 65 | return 66 | 67 | def verify_and_decode(self, eudcc: bytes) -> Union[Dict[int, Any], bytes]: 68 | if eudcc.startswith(b"HC1:"): 69 | # Decode Base45 data. 70 | eudcc = b45decode(eudcc[4:]) 71 | # Decompress with zlib. 72 | eudcc = zlib.decompress(eudcc) 73 | # Verify and decode CWT. 74 | return cwt.decode(eudcc, keys=self._dscs) 75 | 76 | def _load_trustlist(self): 77 | try: 78 | with open(self._trustlist_store_path) as f: 79 | self._trustlist = json.load(f) 80 | except Exception as err: 81 | if type(err) is not FileNotFoundError: 82 | raise err 83 | self._trustlist = [] 84 | return 85 | 86 | 87 | # An endpoint of Digital Green Certificate Verifier Service compliant with: 88 | # https://eu-digital-green-certificates.github.io/dgca-verifier-service/ 89 | BASE_URL = os.environ["CWT_SAMPLES_EUDCC_BASE_URL"] 90 | 91 | # e.g., "./dscs.json" 92 | TRUSTLIST_STORE_PATH = os.environ["CWT_SAMPLES_EUDCC_TRUSTLIST_STORE_PATH"] 93 | 94 | # quoted from https://github.com/eu-digital-green-certificates/dgc-testdata/blob/main/AT/2DCode/raw/1.json 95 | BASE45_FORMATTED_EUDCC = b"HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH-R6IOOK.IR9B+9G4G50PHZF0AT4V22F/8X*G3M9JUPY0BX/KR96R/S09T./0LWTKD33236J3TA3M*4VV2 73-E3GG396B-43O058YIB73A*G3W19UEBY5:PI0EGSP4*2DN43U*0CEBQ/GXQFY73CIBC:G 7376BXBJBAJ UNFMJCRN0H3PQN*E33H3OA70M3FMJIJN523.K5QZ4A+2XEN QT QTHC31M3+E32R44$28A9H0D3ZCL4JMYAZ+S-A5$XKX6T2YC 35H/ITX8GL2-LH/CJTK96L6SR9MU9RFGJA6Q3QR$P2OIC0JVLA8J3ET3:H3A+2+33U SAAUOT3TPTO4UBZIC0JKQTL*QDKBO.AI9BVYTOCFOPS4IJCOT0$89NT2V457U8+9W2KQ-7LF9-DF07U$B97JJ1D7WKP/HLIJL8JF8JFHJP7NVDEBU1J*Z222E.GJ457661CFFTWM-8P2IUE7K*SSW613:9/:TT5IYQBTBU16R4I1A/9VRPJ-TS.7ZEM7MSVOCD4RG2L-TQJROXL2J:52J7F0Q10SMAP3CG3KHF0DWIH" 96 | # RAW_EUDCC = bytes.fromhex( 97 | # "d2844da20448d919375fc1e7b6b20126a0590133a4041a61817ca0061a60942ea001624154390103a101a4617681aa62646e01626d616d4f52472d3130303033303231356276706a313131393334393030376264746a323032312d30322d313862636f624154626369783155524e3a555643493a30313a41543a31303830373834334639344145453045453530393346424332353442443831332342626d706c45552f312f32302f31353238626973781b4d696e6973747279206f66204865616c74682c20417573747269616273640262746769383430353339303036636e616da463666e74754d5553544552465241553c474f455353494e47455262666e754d7573746572667261752d47c3b6c39f696e67657263676e74684741425249454c4562676e684761627269656c656376657265312e302e3063646f626a313939382d30322d323658405812fce67cb84c3911d78e3f61f890d0c80eb9675806aebed66aa2d0d0c91d1fc98d7bcb80bf00e181806a9502e11b071325901bd0d2c1b6438747b8cc50f521" 98 | # ) 99 | 100 | if __name__ == "__main__": 101 | v = Verifier.new(BASE_URL, TRUSTLIST_STORE_PATH) 102 | v.refresh_trustlist() 103 | try: 104 | res = v.verify_and_decode(BASE45_FORMATTED_EUDCC) # or RAW_EUDCC 105 | except Exception as err: 106 | print("Verification failed: %s" % err) 107 | exit(1) 108 | hcert = res[-260] 109 | print(hcert) 110 | exit(0) 111 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dajiaji/python-cwt/2d5f5c6bda76545c0f2d615424811c63162b09c3/tests/__init__.py -------------------------------------------------------------------------------- /tests/keys/cacert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDrzCCApegAwIBAgIUIK/CYzdq4BLLVXqSclNBgXy6mgswDQYJKoZIhvcNAQEL 3 | BQAwZjELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMRAwDgYDVQQKDAdkYWpp 4 | YWppMRMwEQYDVQQDDApweXRob24tY3d0MSAwHgYJKoZIhvcNAQkBFhFkYWppYWpp 5 | QGdtYWlsLmNvbTAgFw0yMTEwMDIyMzU0NTZaGA8yMDcxMDkyMDIzNTQ1NlowZjEL 6 | MAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMRAwDgYDVQQKDAdkYWppYWppMRMw 7 | EQYDVQQDDApweXRob24tY3d0MSAwHgYJKoZIhvcNAQkBFhFkYWppYWppQGdtYWls 8 | LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANFg4sw+uPWbPBbk 9 | JuohXc89O0gaqG1H2i1wzxxka32XNKIdwrxOJvsB2eALo3q7dTqLKCgzrjdd5N07 10 | gi0KzqjoIXIXqKpV5tm0fP5gCzEOWgxySCfBJOJyyvO6WvYXdvukEBnL+48D8RSj 11 | QH9fQEju5RG0taFZE+0nQ7n3P0J+Q+OfBUEoRiHvCd8oUx0s+fBpKdfhMAbD1sGA 12 | Q9CokUFeWc49em8inNqia5xljBtSYo6/2Zx9eb7B53wvBC0EmtS4SRyksR2emlr6 13 | GxMj/EZW7hcTfZCM4V2JYXliuAEdxA0sB7q+WqLg4OvltBQxCBgTTEXRCzxj3XXZ 14 | y7QyUacCAwEAAaNTMFEwHQYDVR0OBBYEFA9id2cL/Chjv6liRN3HD849TARsMB8G 15 | A1UdIwQYMBaAFA9id2cL/Chjv6liRN3HD849TARsMA8GA1UdEwEB/wQFMAMBAf8w 16 | DQYJKoZIhvcNAQELBQADggEBAArIej5eJN1OmD3l3ef9QzosCxKThNwqNY55CoSS 17 | C3IRl+IAXy9Lvx7cgiliwBgCv99RbXZ1ZnptTHC/1kzMzPhPg9pGKDowFP+rywaB 18 | 9+NTuHTWQ4hkKDsru5dpf75ILNI5PTUi1iiBM7TdgSerpEVroUWZiOpGAdlKkmE1 19 | h4gkR6eQY9Q0IvVXwagy/PPoQ1XO1i5Hyg3aXeDZBgkE7AuW9uxtYQHzg8JG2TNk 20 | o/yp497yf/Ew4t6KzGDhSa8L1euMPtclALDWFhgl6WmYsHOqAOsyZOLwpsifWa53 21 | 3wI9mtTvLEg8TFKMOdU0sbAoQSbrrI9m4QS7mzDLchngj3E= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /tests/keys/cacert_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDuTCCAqGgAwIBAgIUePmhPW35yvynoMYLKF60ml8lymMwDQYJKoZIhvcNAQEL 3 | BQAwazELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQKDAhkYWpp 4 | YWppMjEVMBMGA1UEAwwMcHl0aG9uLWN3dC0yMSIwIAYJKoZIhvcNAQkBFhNkYWpp 5 | YWppQGV4YW1wbGUuY29tMCAXDTIxMTAwMzEzMTMwNFoYDzIwNzEwOTIxMTMxMzA0 6 | WjBrMQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xETAPBgNVBAoMCGRhamlh 7 | amkyMRUwEwYDVQQDDAxweXRob24tY3d0LTIxIjAgBgkqhkiG9w0BCQEWE2Rhamlh 8 | amlAZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1 9 | S6QV5d7fiJADa/xcq/RFCrFXzVHFw4xQGAekGQVh6ZUF3cl9T+0aYDRdA1sibz8d 10 | Ia3UAWY2o1ugwz3m/c3BAgwvn8rVckW14ZtDJc7HWq5g7tuiYrjKfcDpDqGXqmfI 11 | zXVfCK5YTZe87acsl7TrQH093N/AYNAwz8O7J2Gib85Eun4GtEKuC9PHCU7bdK1t 12 | FvdSvNyvrfzaw18TVfSjdx+CwuILcU5gb0v+hTd67coFYfg+xLXkA+doghofUkLz 13 | IfUafRN2tJwjX7GnWLE7yPqJ3zSQhqP6+pFd7TzlQv1/IdgzaAP/lvdqMWGp8wuo 14 | BOFGhlOr82dNdSX43mzHAgMBAAGjUzBRMB0GA1UdDgQWBBST9k7q75BqhZOBq4Jl 15 | IPdcqpH6gDAfBgNVHSMEGDAWgBST9k7q75BqhZOBq4JlIPdcqpH6gDAPBgNVHRMB 16 | Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBzayHQCzak0UDsiCAdzLjDjH8z 17 | B1JmAI6imuJaCSADBX8UJvCpqt6R0F1lfw15b4DDkdMEhXnz2/i/nQ1AkuA6yVDV 18 | 5YYLPkxeRFcUakY/AUDjYO0DBRKwknSJariUaMPq2Qm6XPZqExqL7fK2dXUS/rYe 19 | O+2w0bUFS4wZCr5pSp+shtIykqQok3/lxfk0TTFS9BTcoCRtw5fZM6u1yCzzSx+t 20 | P1XnlAnl5WUZH+Nu1IMrznttYMhsqJJp9pZzioxme98OYgJ42aCpOE0aFvEVHdY7 21 | ry+P6jFp3eZmjC656MnSMXIc5dIGWBMAP85fXlUsGMtW+oqbcN15BDTXuTOz 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /tests/keys/cakey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIG4H344/8+YoCAggA 3 | MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECOljU6B1PT3EBIIEyMxWO48ncwZL 4 | S6Erk8JLZU+Bku7Z7Q4b3y01ZufaSkGFMWz1Z8doeGQWgCybyFTJbAXcO+Cykhmd 5 | BwFGrS9nGclfnvUQdKb+xJ76plw32/3ZG90fg/IxNL+hHZZ4BR9dVp5TK+qF0DzP 6 | h7TqYImhayy22Y7BUW/GlWN6eaoSFmH5qVgRP8WErW8FQGz2CcHh0NFuAlgnnD8p 7 | JFPxdxBkGg2ipuEkQbcW7cFU9caTGC6F6lxp8B3/QGemunHEx0ZbSeTVUvMU2fgA 8 | vPxMb/MUKFhielm0RXhsWcnfQuklji1zuOS3u4aY9DhVedH+7AjDiPiQucJlTIJq 9 | z8GUUK9n66XEO7hgLiqxwpy9VrlSCKudzJGoGUNxTzxkzp58ecImVrMqJj6wC1kq 10 | j4/L7C/ILvkEbX8WngUB3XuVucEzTszXgSaPQRkScf2L/sCrZ2gAZ70T1RYewAsU 11 | Zd5Ysq9vljGTGPNgM51wRpgLGDlSh+PYNg1lQ0DmuJin05raP/OkZB8df+dfRpEn 12 | ld90YYpzmoPwWpf9uhxgv6E82d2RICY9Da80T8pQ+7tvT2B27FeSEhYCw1A+93gC 13 | yNKwGoPwvaHuek+UOv8OmkdGosQnzkEYsPQfJu/2OVis6eI83ZS/QKEt1LKQE/VG 14 | RO33uzLHYN1KrQEwTLaQ8FQeFyLX93cJJr29ZJkkqye3W/LoKbsvBVEUodF5BzGf 15 | 13INbMgtUsGLwOrIu2v5tUELX57bF1+HGf28J1tFawcQXTv54otFkt1D9zuSknRo 16 | 6D9dm9I1kkgWGmXjRHUo6aAyAhrIIJOBm/iHgCg8AMlAAfEtePffZj/aRg0epEO3 17 | Ho6vdEB1/jTTCYNzwR3QsxvBGKG153tt6LYN//pMaKWiJ4t6TlNvPQYPhovHiTJc 18 | cWvIYtSWhHZYbQvCB5SfjjaZJRvFWbivOEIRg51+qRgygxGyw6Si69+qeJhIGdDz 19 | KriC6yFzECiFGz+dYdInWBhubrLKXjcImj1t6haZW94wMt9RmWoXFGnAcbmmHbvR 20 | Z9jHEwxbCLdl9wJz4XNBQgsSZQq8n/IdPIZ39tkAtzwFScXwPKNj+m8vypJSrBRr 21 | 2m3pvB+iZ56/7JPhTRM3l+LrJMtz6U57pktRoUsTzaaRsG0HCjJRORMSBjMev7Wr 22 | PYvDa1HU4YaAUgFOWiBM7MsBLf9ipXq87GnOr+wp4l+wALtbWEu0MwX+rKqpi5Tf 23 | XR9mCy06/6xLe7EaB6TsN8uQfcRIx7HzTtqt8h4ReDvgNyA944YioNa6VZAj0bl+ 24 | Ho5/ehihmsF4TyRkuFUOuSWoLJIqHVP42XxH9hP/r3AgtKsttQQQVNPWJ1piaVER 25 | IjRp89BiGtJT/2PdQ4sPbawK/vAaRzxUFNdg7IBXS34eNyJd23V5WkGt5sd1xk+l 26 | RVkyvqNNi4Zw5NBWWNPJcnKXhheWgY6EkgiT/M6vZkBhvVGgRYGo/ikorROrhKjj 27 | rVoOwIc+jMAnrKNg8v5JH0usWgbDQvquDs6bC42DfkXSFR1CYci067bUPrIOXXOs 28 | FQQZtP0ndF5Fq/KcVWHa7dKUcYS0b9WAlI+61br16R5eAS6qqItoQYzAc5xLDUmq 29 | xq1eW01xvHvfv0coOdjuuw== 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /tests/keys/cert_es256.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIClDCCAXygAwIBAgIBBDANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJKUDEO 3 | MAwGA1UECAwFVG9reW8xEDAOBgNVBAoMB2RhamlhamkxEzARBgNVBAMMCnB5dGhv 4 | bi1jd3QxIDAeBgkqhkiG9w0BCQEWEWRhamlhamlAZ21haWwuY29tMB4XDTIxMTAw 5 | MzEzMDE1MFoXDTMxMTAwMTEzMDE1MFowZDELMAkGA1UEBhMCSlAxDjAMBgNVBAgM 6 | BVRva3lvMQ0wCwYDVQQKDAR0ZXN0MRUwEwYDVQQDDAx0ZXN0LmV4YW1wbGUxHzAd 7 | BgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjO 8 | PQMBBwNCAASg40K/VCiJqMhtxbbUOrLoSca8FO4XboZTt3LonM59ebJepO+ytE0t 9 | +hVvk8hBix2NeKL2X3WfQ2VrGwPAUxxnoxowGDAJBgNVHRMEAjAAMAsGA1UdDwQE 10 | AwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAZFfvFbaDk/DmG2cPGTwqwnFok1QnH2Tz 11 | kjk7p4vs1ycWzEDltkhyzcJxTSHoQGdykf7fG8NCrEqfi1G3hOyAtGxVIVcqsI+K 12 | IJCESp43zrNz5HsbwEY8l5rvcwohKGlE/idIFt5IuDTv7vsg/FaCIDeruw0NrXAA 13 | CnLTwksawsxaCvtY12U0wsI2aC2Sb6V3HL+OLgcN6ZWzZ054L88JllckYnqJB8wC 14 | VBzzX2K2sZH3yeS39oRWZOVG6fwXsX4k0fHFx+Fn6KlrBU15pbjMLMn0ow0X3Y8e 15 | 7FOgfkkph+N7e2SxceXNjrLiumOdclPm9yGSWoGsOJdId53dPvqAsQ== 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /tests/keys/certs/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGTCCAb6gAwIBAgIUfRDfKDU6Ci55bkn26i7WYFYh3q0wCgYIKoZIzj0EAwIw 3 | WDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5 4 | YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUw 5 | MzE2MDA0NzAyWhcNMzUwMzE0MDA0NzAyWjBYMQswCQYDVQQGEwJKUDEOMAwGA1UE 6 | CAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMREwDwYDVQQKDAhNeVJvb3RDQTET 7 | MBEGA1UEAwwKTXkgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNOf 8 | mjPoj91hVpZp35AReTdyzpscS9ZZYJrfPk33capHVoZVURyhBFj5DYqY+EvbPZCx 9 | vwPThpI2CU2HPLeICj2jZjBkMB0GA1UdDgQWBBQdn2hb29ekyauM/BaNqyUNha2o 10 | szAfBgNVHSMEGDAWgBQdn2hb29ekyauM/BaNqyUNha2oszASBgNVHRMBAf8ECDAG 11 | AQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNJADBGAiEA/IO5Fp9d 12 | 1Dp+JxECul0Wn9l1Silqpez0mwq1c6a3iOACIQCMgs8iLS75HGqDqKFtsAf8Mu9U 13 | aG+b3xsVAO1E4MFj2w== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /tests/keys/certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGTCCAb6gAwIBAgIUfRDfKDU6Ci55bkn26i7WYFYh3q0wCgYIKoZIzj0EAwIw 3 | WDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5 4 | YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUw 5 | MzE2MDA0NzAyWhcNMzUwMzE0MDA0NzAyWjBYMQswCQYDVQQGEwJKUDEOMAwGA1UE 6 | CAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMREwDwYDVQQKDAhNeVJvb3RDQTET 7 | MBEGA1UEAwwKTXkgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNOf 8 | mjPoj91hVpZp35AReTdyzpscS9ZZYJrfPk33capHVoZVURyhBFj5DYqY+EvbPZCx 9 | vwPThpI2CU2HPLeICj2jZjBkMB0GA1UdDgQWBBQdn2hb29ekyauM/BaNqyUNha2o 10 | szAfBgNVHSMEGDAWgBQdn2hb29ekyauM/BaNqyUNha2oszASBgNVHRMBAf8ECDAG 11 | AQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNJADBGAiEA/IO5Fp9d 12 | 1Dp+JxECul0Wn9l1Silqpez0mwq1c6a3iOACIQCMgs8iLS75HGqDqKFtsAf8Mu9U 13 | aG+b3xsVAO1E4MFj2w== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /tests/keys/certs/ca_another.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGTCCAb6gAwIBAgIUOcRjF2wAY9MtpveGJhrJKzp+e3YwCgYIKoZIzj0EAwIw 3 | WDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5 4 | YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUw 5 | MzE2MDEwODQ2WhcNMzUwMzE0MDEwODQ2WjBYMQswCQYDVQQGEwJKUDEOMAwGA1UE 6 | CAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMREwDwYDVQQKDAhNeVJvb3RDQTET 7 | MBEGA1UEAwwKTXkgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLia 8 | 8183LhcQhXjf8r9RWKuaMhZaXeNfjO8IYVja1seCdl/uNmS00j9eyJQlPsSbDID7 9 | qdXIkN9QSTKYuQW2+qejZjBkMB0GA1UdDgQWBBSPv8mstGZ9xK+kULK/0kKLYKj+ 10 | SDAfBgNVHSMEGDAWgBSPv8mstGZ9xK+kULK/0kKLYKj+SDASBgNVHRMBAf8ECDAG 11 | AQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNJADBGAiEAomhtSi11 12 | lPV+NUSULLyraQID76yI404MUyK/IxXdZjACIQDG+X1V7c/d07/NrPSi0+e3+6po 13 | zUWNKdtIe0UxLXiKXg== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /tests/keys/certs/ca_another.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGTCCAb6gAwIBAgIUOcRjF2wAY9MtpveGJhrJKzp+e3YwCgYIKoZIzj0EAwIw 3 | WDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5 4 | YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUw 5 | MzE2MDEwODQ2WhcNMzUwMzE0MDEwODQ2WjBYMQswCQYDVQQGEwJKUDEOMAwGA1UE 6 | CAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMREwDwYDVQQKDAhNeVJvb3RDQTET 7 | MBEGA1UEAwwKTXkgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLia 8 | 8183LhcQhXjf8r9RWKuaMhZaXeNfjO8IYVja1seCdl/uNmS00j9eyJQlPsSbDID7 9 | qdXIkN9QSTKYuQW2+qejZjBkMB0GA1UdDgQWBBSPv8mstGZ9xK+kULK/0kKLYKj+ 10 | SDAfBgNVHSMEGDAWgBSPv8mstGZ9xK+kULK/0kKLYKj+SDASBgNVHRMBAf8ECDAG 11 | AQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNJADBGAiEAomhtSi11 12 | lPV+NUSULLyraQID76yI404MUyK/IxXdZjACIQDG+X1V7c/d07/NrPSi0+e3+6po 13 | zUWNKdtIe0UxLXiKXg== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /tests/keys/certs/ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIPE40NcR4PbIFv6IDd2ZkHX7pnHkgPq0FCbarHVeKbxFoAoGCCqGSM49 3 | AwEHoUQDQgAE05+aM+iP3WFWlmnfkBF5N3LOmxxL1llgmt8+TfdxqkdWhlVRHKEE 4 | WPkNipj4S9s9kLG/A9OGkjYJTYc8t4gKPQ== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/keys/certs/create_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create a self-signed root CA certificate, server certificate, and convert them to PEM format 3 | # The server certificate is signed by the root CA certificate 4 | # The root CA certificate is created with CA:TRUE, keyCertSign, and cRLSign extensions 5 | # The server certificate is created with the subjectAltName extension 6 | 7 | # Create a self-signed root CA certificate 8 | openssl ecparam -name prime256v1 -genkey -noout -out ca.key 9 | openssl ec -in ca.key -out ca_key.der -outform DER 10 | openssl ec -inform DER -in ca_key.der -out ca_key.pem -outform PEM 11 | openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config openssl_ca.cnf 12 | openssl x509 -in ca.crt -text -noout 13 | openssl x509 -in ca.crt -out ca.der -outform DER 14 | openssl x509 -inform DER -in ca.der -out ca.pem -outform PEM 15 | 16 | # Create a server certificate signed by the root CA certificate 17 | openssl ecparam -name prime256v1 -genkey -noout -out server.key 18 | openssl ec -in server.key -out server_key.der -outform DER 19 | openssl ec -inform DER -in server_key.der -out server_key.pem -outform PEM 20 | openssl req -new -key server.key -out server.csr -config openssl_server.cnf 21 | openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256 -extfile openssl_server.cnf -extensions v3_req 22 | openssl x509 -in server.crt -text -noout 23 | openssl x509 -in server.crt -out server.der -outform DER 24 | openssl x509 -inform DER -in server.der -out server.pem -outform PEM 25 | -------------------------------------------------------------------------------- /tests/keys/certs/openssl_ca.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = req_distinguished_name 6 | x509_extensions = v3_ca 7 | 8 | [ req_distinguished_name ] 9 | C = JP 10 | ST = Tokyo 11 | L = Setagaya 12 | O = MyRootCA 13 | CN = My Root CA 14 | 15 | [ v3_ca ] 16 | subjectKeyIdentifier = hash 17 | authorityKeyIdentifier = keyid:always,issuer 18 | basicConstraints = critical, CA:TRUE, pathlen:0 19 | keyUsage = critical, keyCertSign, cRLSign 20 | -------------------------------------------------------------------------------- /tests/keys/certs/openssl_server.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = req_distinguished_name 6 | req_extensions = v3_req 7 | 8 | [ req_distinguished_name ] 9 | C = JP 10 | ST = Tokyo 11 | L = Setagaya 12 | O = MyCompany 13 | CN = test.example 14 | 15 | [ v3_req ] 16 | basicConstraints = critical, CA:FALSE 17 | keyUsage = critical, digitalSignature, keyEncipherment 18 | subjectAltName = @alt_names 19 | 20 | [ alt_names ] 21 | DNS.1 = test.example 22 | DNS.2 = www.test.example 23 | DNS.3 = sub.test.example 24 | -------------------------------------------------------------------------------- /tests/keys/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICUzCCAfqgAwIBAgIUDzPXnaKASVKKAUnFWzAB3wjGekMwCgYIKoZIzj0EAwIw 3 | WDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5 4 | YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUw 5 | MzE2MDA0NzMyWhcNMjYwMzE2MDA0NzMyWjBbMQswCQYDVQQGEwJKUDEOMAwGA1UE 6 | CAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMRIwEAYDVQQKDAlNeUNvbXBhbnkx 7 | FTATBgNVBAMMDHRlc3QuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA 8 | BPm5796++AOpcp8WLRixIZOrbr9Pxn5f1SE3HQT1hl1WM9GCfuWeTkyOhqLVqC4x 9 | FfadOvCqE9EWCfby6UbtJe+jgZ4wgZswDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8E 10 | BAMCBaAwOwYDVR0RBDQwMoIMdGVzdC5leGFtcGxlghB3d3cudGVzdC5leGFtcGxl 11 | ghBzdWIudGVzdC5leGFtcGxlMB0GA1UdDgQWBBQR0Zcw4IV5KZbPWXe6PY64dsvg 12 | 4zAfBgNVHSMEGDAWgBQdn2hb29ekyauM/BaNqyUNha2oszAKBggqhkjOPQQDAgNH 13 | ADBEAiB+tzD6UBdU+C8Q4hb9dnCXHNkLEAsb2j67+p5uJrwd9AIgFR1MzLdwDAql 14 | IPn83oVFyGBSIn4WyaGr/zRXydew80s= 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /tests/keys/certs/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "P-256", 5 | "kid": "P-256-01", 6 | "x": "-bnv3r74A6lynxYtGLEhk6tuv0_Gfl_VITcdBPWGXVY", 7 | "y": "M9GCfuWeTkyOhqLVqC4xFfadOvCqE9EWCfby6UbtJe8", 8 | "x5c": [ 9 | "MIICUzCCAfqgAwIBAgIUDzPXnaKASVKKAUnFWzAB3wjGekMwCgYIKoZIzj0EAwIwWDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUwMzE2MDA0NzMyWhcNMjYwMzE2MDA0NzMyWjBbMQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMRIwEAYDVQQKDAlNeUNvbXBhbnkxFTATBgNVBAMMDHRlc3QuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPm5796--AOpcp8WLRixIZOrbr9Pxn5f1SE3HQT1hl1WM9GCfuWeTkyOhqLVqC4xFfadOvCqE9EWCfby6UbtJe-jgZ4wgZswDAYDVR0TAQH_BAIwADAOBgNVHQ8BAf8EBAMCBaAwOwYDVR0RBDQwMoIMdGVzdC5leGFtcGxlghB3d3cudGVzdC5leGFtcGxlghBzdWIudGVzdC5leGFtcGxlMB0GA1UdDgQWBBQR0Zcw4IV5KZbPWXe6PY64dsvg4zAfBgNVHSMEGDAWgBQdn2hb29ekyauM_BaNqyUNha2oszAKBggqhkjOPQQDAgNHADBEAiB-tzD6UBdU-C8Q4hb9dnCXHNkLEAsb2j67-p5uJrwd9AIgFR1MzLdwDAqlIPn83oVFyGBSIn4WyaGr_zRXydew80s", 10 | "MIICGTCCAb6gAwIBAgIUfRDfKDU6Ci55bkn26i7WYFYh3q0wCgYIKoZIzj0EAwIwWDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUwMzE2MDA0NzAyWhcNMzUwMzE0MDA0NzAyWjBYMQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMREwDwYDVQQKDAhNeVJvb3RDQTETMBEGA1UEAwwKTXkgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNOfmjPoj91hVpZp35AReTdyzpscS9ZZYJrfPk33capHVoZVURyhBFj5DYqY-EvbPZCxvwPThpI2CU2HPLeICj2jZjBkMB0GA1UdDgQWBBQdn2hb29ekyauM_BaNqyUNha2oszAfBgNVHSMEGDAWgBQdn2hb29ekyauM_BaNqyUNha2oszASBgNVHRMBAf8ECDAGAQH_AgEAMA4GA1UdDwEB_wQEAwIBBjAKBggqhkjOPQQDAgNJADBGAiEA_IO5Fp9d1Dp-JxECul0Wn9l1Silqpez0mwq1c6a3iOACIQCMgs8iLS75HGqDqKFtsAf8Mu9UaG-b3xsVAO1E4MFj2w" 11 | ], 12 | "alg": "ES256" 13 | } 14 | -------------------------------------------------------------------------------- /tests/keys/certs/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICUzCCAfqgAwIBAgIUDzPXnaKASVKKAUnFWzAB3wjGekMwCgYIKoZIzj0EAwIw 3 | WDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5 4 | YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUw 5 | MzE2MDA0NzMyWhcNMjYwMzE2MDA0NzMyWjBbMQswCQYDVQQGEwJKUDEOMAwGA1UE 6 | CAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMRIwEAYDVQQKDAlNeUNvbXBhbnkx 7 | FTATBgNVBAMMDHRlc3QuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA 8 | BPm5796++AOpcp8WLRixIZOrbr9Pxn5f1SE3HQT1hl1WM9GCfuWeTkyOhqLVqC4x 9 | FfadOvCqE9EWCfby6UbtJe+jgZ4wgZswDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8E 10 | BAMCBaAwOwYDVR0RBDQwMoIMdGVzdC5leGFtcGxlghB3d3cudGVzdC5leGFtcGxl 11 | ghBzdWIudGVzdC5leGFtcGxlMB0GA1UdDgQWBBQR0Zcw4IV5KZbPWXe6PY64dsvg 12 | 4zAfBgNVHSMEGDAWgBQdn2hb29ekyauM/BaNqyUNha2oszAKBggqhkjOPQQDAgNH 13 | ADBEAiB+tzD6UBdU+C8Q4hb9dnCXHNkLEAsb2j67+p5uJrwd9AIgFR1MzLdwDAql 14 | IPn83oVFyGBSIn4WyaGr/zRXydew80s= 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /tests/keys/certs/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIHJtE8+fZdE5gtsz3cwMyc/yqdh4T8vA1Oaqnw9l8hNzoAoGCCqGSM49 3 | AwEHoUQDQgAE+bnv3r74A6lynxYtGLEhk6tuv0/Gfl/VITcdBPWGXVYz0YJ+5Z5O 4 | TI6GotWoLjEV9p068KoT0RYJ9vLpRu0l7w== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/keys/certs/server_without_root.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "P-256", 5 | "kid": "P-256-01", 6 | "x": "-bnv3r74A6lynxYtGLEhk6tuv0_Gfl_VITcdBPWGXVY", 7 | "y": "M9GCfuWeTkyOhqLVqC4xFfadOvCqE9EWCfby6UbtJe8", 8 | "x5c": [ 9 | "MIICUzCCAfqgAwIBAgIUDzPXnaKASVKKAUnFWzAB3wjGekMwCgYIKoZIzj0EAwIwWDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMREwDwYDVQQHDAhTZXRhZ2F5YTERMA8GA1UECgwITXlSb290Q0ExEzARBgNVBAMMCk15IFJvb3QgQ0EwHhcNMjUwMzE2MDA0NzMyWhcNMjYwMzE2MDA0NzMyWjBbMQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xETAPBgNVBAcMCFNldGFnYXlhMRIwEAYDVQQKDAlNeUNvbXBhbnkxFTATBgNVBAMMDHRlc3QuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPm5796--AOpcp8WLRixIZOrbr9Pxn5f1SE3HQT1hl1WM9GCfuWeTkyOhqLVqC4xFfadOvCqE9EWCfby6UbtJe-jgZ4wgZswDAYDVR0TAQH_BAIwADAOBgNVHQ8BAf8EBAMCBaAwOwYDVR0RBDQwMoIMdGVzdC5leGFtcGxlghB3d3cudGVzdC5leGFtcGxlghBzdWIudGVzdC5leGFtcGxlMB0GA1UdDgQWBBQR0Zcw4IV5KZbPWXe6PY64dsvg4zAfBgNVHSMEGDAWgBQdn2hb29ekyauM_BaNqyUNha2oszAKBggqhkjOPQQDAgNHADBEAiB-tzD6UBdU-C8Q4hb9dnCXHNkLEAsb2j67-p5uJrwd9AIgFR1MzLdwDAqlIPn83oVFyGBSIn4WyaGr_zRXydew80s" 10 | ], 11 | "alg": "ES256" 12 | } 13 | -------------------------------------------------------------------------------- /tests/keys/hcert_testdata_cert_at.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBvTCCAWOgAwIBAgIKAXk8i88OleLsuTAKBggqhkjOPQQDAjA2MRYwFAYDVQQDDA1BVCBER0MgQ1NDQSAxMQswCQYDVQQGEwJBVDEPMA0GA1UECgwGQk1TR1BLMB4XDTIxMDUwNTEyNDEwNloXDTIzMDUwNTEyNDEwNlowPTERMA8GA1UEAwwIQVQgRFNDIDExCzAJBgNVBAYTAkFUMQ8wDQYDVQQKDAZCTVNHUEsxCjAIBgNVBAUTATEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASt1Vz1rRuW1HqObUE9MDe7RzIk1gq4XW5GTyHuHTj5cFEn2Rge37+hINfCZZcozpwQKdyaporPUP1TE7UWl0F3o1IwUDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFO49y1ISb6cvXshLcp8UUp9VoGLQMB8GA1UdIwQYMBaAFP7JKEOflGEvef2iMdtopsetwGGeMAoGCCqGSM49BAMCA0gAMEUCIQDG2opotWG8tJXN84ZZqT6wUBz9KF8D+z9NukYvnUEQ3QIgdBLFSTSiDt0UJaDF6St2bkUQuVHW6fQbONd731/M4nc= 3 | -----END CERTIFICATE----- 4 | -------------------------------------------------------------------------------- /tests/keys/hs256.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "oct", 3 | "use": "sig", 4 | "kid": "HS256-01", 5 | "k": "l6WRtQu5RJx0IF_POv0pcPilEMM5KUdlw8jo3VBmvvLucQ6uFxSEXhZU4S8hdgQGjPDOk8NfVNa9PPYYnWJJHSaTQl97Bmjr6BV3Z4wPZlIP8pT3IDLM7IwJPNrsE3IWr1d-n8SB_Q-nX9NPte4h8NjZrfM3c_8ne5gcEqjh5J5era9GkRDlBGAN_fqmY-f1AvBz3vxW_raU9EixvROqcZiywgt06aQq6ErjW-edzyLHDU4fLsGPavjUOukLBzByiB5TGMypJuMj_4LBUs1x7jdnAIJeVVQvH5gtHTMlBp95WzJUnWFifriwcVKK-CtXo8Og6HIioihI95bQ6lwNYg", 6 | "alg": "HS256" 7 | } 8 | -------------------------------------------------------------------------------- /tests/keys/hs384.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "oct", 3 | "use": "sig", 4 | "kid": "HS384-01", 5 | "k": "vpdeao6CC_ze7hgqROayslNs0k_SydKu6XUto0AI6UWx5PbIN8mqbr6aH2ikwYcX0f_ufZHDYW_9x6-DkQxm4--vkoSBLrCZ2iEuriQv4CywwdK1_IAjrVWXg0B3pku3oZv3Q7PIZ-V4Mi5Ad8lYyuNQb-jEg8b0cz0Gtfr0CSqI1PzlRefGJmpL70NrtxL1CPZ-mUhPRsuj25nYXxndq-xBw-n6e0Pe2OExRkJ5kYOnsSwSFWE2EYP8L81vcyN8YcPCXZFBOlkjPuZwIJVWGnfTPW7XoIKXozhbUAn9SmrscwZOs9pTOEvS_uoaaHIWGT90Dwt4kM0S0sxV9OZFOg", 6 | "alg": "HS384" 7 | } 8 | -------------------------------------------------------------------------------- /tests/keys/hs512.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "oct", 3 | "use": "sig", 4 | "kid": "HS512-01", 5 | "k": "wN5Nff-jsp5DAGEwNrukAHffbuUa8jM0yTOjJ5Sy2pzYP5jyQly356yizDceCl1zc0VYK82g4HvtZ1K0fFJRzdpBbfBbvvOR7-1bzHxVUJcK9TgTH_WSNtu_95cNsQcJ3CFK8ako3ngBkyHz532erBptHhJ_qAH5wrmdAZNOr1UYPObddsJ_5uOmUEy0Z7KO1Ot5z9fKxNJSSXMPLAcsLDZZWH1nqwR3PIWNrUndqAYs8BA82W5XPrImYXP1R3ISfR0NOAu1uXRbBHVLIHJOClLnA1KDElBLXtnLwdXjHCFYrfR9XVwDe7MYOYsCYqRKKl_Iidx-SvLshSFXORUbuQ", 6 | "alg": "HS512" 7 | } 8 | -------------------------------------------------------------------------------- /tests/keys/private_key_cert_es256.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9Q1/DvAMdOeB7KBD 3 | H5BEHbJAbkNIg7ZiAGIpp8PLf2OhRANCAASg40K/VCiJqMhtxbbUOrLoSca8FO4X 4 | boZTt3LonM59ebJepO+ytE0t+hVvk8hBix2NeKL2X3WfQ2VrGwPAUxxn 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/keys/private_key_ed25519.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "d": "L8JS08VsFZoZxGa9JvzYmCWOwg7zaKcei3KZmYsj7dc", 4 | "use": "sig", 5 | "crv": "Ed25519", 6 | "kid": "Ed25519-01", 7 | "x": "2E6dX83gqD_D0eAmqnaHe1TC1xuld6iAKXfw2OVATr0" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/private_key_ed25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEIELGddB8LQfnKY0cExSijUZDMd8zc1EEOXwUwe0B5dup 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/private_key_ed448.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "d": "vOHg3x9AXEBRDnzM5b68bLFswieywpJzTOkxafU5fiDxyKowuetnBgjQsgTRWoc067X9xvZWE0Sd", 4 | "use": "sig", 5 | "crv": "Ed448", 6 | "kid": "Ed448-01", 7 | "x": "25isUWIosUkM2ynOPFP5t7BbwM1_iFQmKBpHvA0hgXpRX6yyu-nq6BBmpS3J0DYTlZIoA4qwgSqA" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/private_key_ed448.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MEcCAQAwBQYDK2VxBDsEOeHvxMbj5IP1NJCcYL4XCY8bD1oXcoMSmVUtq27U3GeB 3 | 65UflXOzcYWcGCZ05URJX/JBpelBIrs6oQ== 4 | -----END PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /tests/keys/private_key_es256.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "d": "kwibx3gas6Kz1V2fyQHKSnr-ybflddSjN0eOnbmLmyo", 4 | "use": "sig", 5 | "crv": "P-256", 6 | "kid": "P-256-01", 7 | "x": "-eZXC6nV-xgthy8zZMCN8pcYSeE2XfWWqckA2fsxHPc", 8 | "y": "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/private_key_es256.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIOkWDKk2jfq81doidWHs95YNFfdf83Jie7HeO5mIr05ooAoGCCqGSM49 3 | AwEHoUQDQgAEp91jKv/CP4v4nDqtzERGnFogBFDvmQw95iB3MQgmutni2+/+uIoS 4 | 8jfLFTqKuRo5MEIaGV683N4NuXPBUPOq3Q== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/keys/private_key_es256k.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "d": "6V-z2tmMoONDgEsLh1_vrENa94eQwA8L5REHvUEEqpQ", 4 | "use": "sig", 5 | "crv": "secp256k1", 6 | "kid": "secp256k1-01", 7 | "x": "6_W-2tVwr9SU0Ts9sFmZgt8JrlxDi8d6G5ltwL2futU", 8 | "y": "mEOme4oiQsXnNwtlfyH5hEkvLDxrtqDUY0UXBIMRuL0" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/private_key_es256k.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHQCAQEEIDVXM+mJxeB6T9kJ39btSTAPn0kJLTANmCVlGnh344HSoAcGBSuBBAAK 3 | oUQDQgAEDFjviK+KpOkRsLCmogaO7+5A8dDzoQHmQYeC4vGcoNo2ABT9EwkH3CwJ 4 | 2hx9Rw/VEbJI6MkFqB/zUT+jIuw3QQ== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /tests/keys/private_key_es384.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "d": "1pImEKbrr771-RKi8Tb7tou_WjiR7kwui_nMu16449rk3lzAqf9buUhTkJ-pogkb", 4 | "use": "sig", 5 | "crv": "P-384", 6 | "kid": "P-384-01", 7 | "x": "_XyN9woHaS0mPimSW-etwJMEDSzxIMjp4PjezavU8SHJoClz1bQrcmPb1ZJxHxhI", 8 | "y": "GCNfc32p9sRotx7u2oDGJ3Eqz6q5zPHLdizNn83oRsUTN31eCWfGLHWRury3xF50" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/private_key_es384.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIGkAgEBBDBRL3j2Ozm3+43DbMEdeDZaNPqZPThOagVn3rQ1ACdWWasskoJ7CNbl 3 | /+PE7qx1PpagBwYFK4EEACKhZANiAATs5tDBLdQ+JbYPnb/iiXFC14+6pOCX0JHN 4 | u5CSLKrUEETDNf6Jv3OuLCaN7/qwwFHJRtTIl9lHuDBalkU6iVUviXxjsp0eDnDx 5 | xO1smUs5LogyBiKypd0XSFcfLXI+Rmc= 6 | -----END EC PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /tests/keys/private_key_es512.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "d": "ADYyo73ZKicOjwGDYQ_ybZKnVzdAcxGm9OVAxQjzgVM4jaS-Iwtkz90oLdDz3shgKlDgtRK2Aa9lMhqR94hBo4IE", 4 | "use": "sig", 5 | "crv": "P-521", 6 | "kid": "P-521-01", 7 | "x": "APkZitSJMJUMB-iPCt47sWu_CrnUHg6IAR4qjmHON-2u41Rjg6DNOS0LZYJJt-AVH5NgGVi8ElIfjo71b9HXCTOc", 8 | "y": "ASx-Cb--149HJ-e1KlSaY-1BOhwOdcTkxSt8BGbW7_hnGfzHsoXM3ywwNcp1Yad-FHUKwmCyMelMQEn2Rh4V2l3I" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/private_key_es512.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIHcAgEBBEIAVuWAE3XBsI7wmEsPxRTFNVS5v9Vv3PoB9pHuhX9hLIIx39wXXtlH 3 | lzNW6d0ic/QS2TokvsOt94AiGDfBoZoeQNKgBwYFK4EEACOhgYkDgYYABAFpUMsq 4 | tASi+WQPMHsKBz59fEtagb04s06QsBDra9JUQnhS9mxORZJTgNBrfNlsBDTYZu6m 5 | S1HPoAE/Z4fMtNHOlQANoDvLrh+OrcKCyI4llCzZ4nTrR13As0nuY9/FAtZj2bTN 6 | uGjHbDcHdWDDkQ1sKbIHABCgN80CTiOsPUyRfqKyNg== 7 | -----END EC PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /tests/keys/private_key_rsa.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": "522lC3SOUd0cPamIQu1mzFIgSJO0zXaS2VhFw9rzvFN3EWzSCawfhURSA7Luf_zy8hNle7fc1ak7rztPbcJYU4kaUs-3s2uSh0OkjFFY-O22MTlSFy08SnUUWjOr6SANdZwXBvwFp_bHo2L5exaoVDbBWWFydAknMdXxesj7uVM", 3 | "kty": "RSA", 4 | "q": "tOU_OTANjWxYkBlXsM0zZnHe2Y-cr6LWoEdXOPf-Qz3qBtODsBBCGEMIWeIMZiYVXWgupTFxUbSvi2XzOSLW_kmzX66Onhg_HxFr6f0GwUC2r0o7DTxkGmImpoGhs-nLY-0lSTJsYWeCRqWvF8Z4lBIScwwJQPNKUrLjM2FeSmc", 5 | "d": "nGQfN_meid33UvmAi811LoiLfIWp5RCN5tC2hGDNJkfSWziFZbeu4B5esnBdoOJET409_brhk6B0eiKmzA10ABY8We1fb9jj9R4u7miL7V6uYUXpysg7MQlWm87wpwkRe1cfKK3yOfuaIUD3cJh9V3TN8Qgw1FJ7EM-40Bpfq-pQiebd9kScYbDBgU6UpdEqGvvN_F1-t3e9cnKF8R3Az6L3MyCZzMP_1VU2ec06D-_js232SZr5wBaUoYGBw6v5EQLlm4uRfBVh830Nj13nYUMBdxFta_vxuVjyT7nQ4lKnr9UegsLNuIdNjzQG-u7YHO51Wz_4XpzhR14oWRQ7xQ", 6 | "e": "AQAB", 7 | "use": "sig", 8 | "kid": "RS256-01", 9 | "qi": "1Zo1K-2F34u9NGVS1FvWT1AYTCCpJgpmnRoQ-sFP20W5q_W2pXKniSFn1de7_swUrDf_gQmTOuWHlMwDfD_vJwaUElRlR6qVUjHaLTjWKQMYUnaVLJEUa6sgON1mYJ3agH-AYZqv1mWGpOt2yEvTisX6lkK5wWWyqKOJhHyTkK8", 10 | "dp": "CBnIayw7RxZMRMRIZr9Ul9ZQFvpEm-SIo175oi9p1K-_PTbn6zrBJ7MKg-Khgo1iG6MeLER1UG1KD4ot75Ob7-CesUNgFMGxMVbmzZqTWLNJa1OsUe9dauXKPpYMcG6Uygcarz3nHMgAmPF_9hUG81uvTOeiT_l6C76HY1rhpM0", 11 | "alg": "RS256", 12 | "dq": "X-RqiHE9retyYyjcAGA20Caq4J-tirmClsJarVthENogVfAIDewAbYYTRjp7IicsCjDxESbNkGd86yNnNLGQUIpXKPCKr6ngxCJjF03HJ-ibLv7loNWTpxzCql9rjcjwxY7vxgaRx2ysdbDcyXivcKbH2u7VdPXDP2WO5SzHZB8", 13 | "n": "o4hWSF-rtXeKI8J32sX3agnJZMyprxQWdmfkaL3jsLK13Yelk7ChlaKIPWU0GHU7Wh_Ce3aslUIb7KsU51Fo3olAVBVszjTYZFu5IIgKtZoni-r8NVHqcmT6JLGhoVGVRaPjEGudmCtxocgiWXGHucl14DChV6PAcPlcbcosNmeJ5z6ia8lp5det-KkjFTe0-EdWB2kTzZvEmbotbtPL-B0BH_jYqmmZWgCU_TZ76QgORiOPmN-GW5RrLOEkay-f-Sx6XGTZxC_heN6eT-Ql1KgGQnagRDOAmcKy4CJp2vmODR8LbOyZl22C8x5wR5Vzz-9NE_WIF0nHT1ivIFOOZQ" 14 | } 15 | -------------------------------------------------------------------------------- /tests/keys/private_key_rsa.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDL1McprDim+OJD 3 | X8pFlgrD/oWp7ytre/fZmTaXfCdgajL1WHVGr/sGsV9wqs8Yv8a1oMC5K47tM5aB 4 | PMVIjdIl6Z1NCkH2FLSqvzKjZtsRMOeMzP72Pkvxyk2wz1K/YQcRlRhxsoE8Mav9 5 | Yxza5GkjxQrsgCIx0gBv5Ue/XnAXza1qhJ8EFJSQSahXZnU85d9A3ooExpHRU39S 6 | o/Q322DwFqIPR7ZBdYvX+PCB20mZCI4QVsDYyaORUFOULM+xZQq8gRgjW278SCN1 7 | AFXO08+XIIE/oYqIxBaH3PljI3ClGA5Ikv172bjB3Fag4csf1Z1tUsLsjELnHsOI 8 | mda94u8XAgMBAAECggEAY/hdIjw8oPAqkR7XJx/68bvQtNL/+byYiBnUI6IZ8fjE 9 | O38CLgQ720F9bNXpstrES+qm+2q4s+/8XjB/FiGwszWMF+/zAA2RurkBzhCxWxIg 10 | TvdwCTfuqY0uY4ybqg+dlusITSNerTYzg5hr25PxWUDYJbyGiObVngvXD63yzkkK 11 | V7/rtkIGwehw17YlO17jzq46ljD3KAokZRvRfn+yN/FwFN7Ps691rAUQ7qFe83CD 12 | 991OSxW0apPHAWDSNUq0iFOJRcCLbaYLVjdcyQxgtQjJAwzp0NOpwRm9fpiD1noY 13 | wZ0ECch0a9dVKxzCxiImDjULTG/nWOUqKuNt9YMMkQKBgQD5YzXRAOQrXmYWPEeS 14 | Gp+H6TLVm5GwcN96VzUAiHsu/+XOvBXNFn/aA6LTw9RDqdOd8tyE5BJ4JruMh2hh 15 | JraFK9Q53sMrpK2osNl26yNz2Gwcaas/Q4NHIxZ7S3+uLVk5yf6Pt9QSgK7FIit5 16 | 0Unt0NyuUAeLzDM68XkSwy/MSQKBgQDRPFc6Nyq7b5Zv+iwHamZE4mcIkUl7ReO3 17 | gqbFButFlt1Zrp6IZ3zrZHJMd6rcxJPawnEtavJ5/7ic3HTi9u08Ig6aShd4Gtjf 18 | c0Bzy/QjL7c4jAl7Oe6Srs7+aYeYEsHay+6UqFNtoBNt170o8axcyVT59Wpg8JfV 19 | x+3btrUgXwKBgFuhqFRzD2Mf/EKPQ3zba2J0vMjfsFg0IOjCwia//wL142pikWAZ 20 | tEBWta05dvSloauXYI8zfdXH5YFs/10y7D1iLUhWIUAX8fbZhA29t/kIvDB0YweL 21 | tuCcc9Y5l2BYKUCih3YnBTUHMzAbrf3EeHV5GJyzqxIipXAy6VmLfjSBAoGAYgBJ 22 | OtMsCK6zX69lf/OuAwWoDLNAH6UUi5d+VTpwaB/JfX/0cR9Hu1lIz9gdB1prhkMc 23 | j5FQESRda9s2RDgz1b596HZbl727Zq2supAEwZZP0wSETFtOCxYljbQP9oKSmdB6 24 | 5tYQfYBEmWY7Czpm6O7EBm+Ua+NEurTyC78ABzECgYEA3LAbZmtIk/ZYqpebGuGM 25 | e6si4Kme8GNb1i0PseDAgYufT8foTphcandADuHRohwLjWgQUZCI7/MRsc+bKdgX 26 | O3onNO+NVw1bAkHUK4r2zLSvHN0/5EYQW1KFoOUf1Ns2OM2xX/6UQCSghq8b6jP0 27 | lx1zZBksUMQsRWgwTPlKqXE= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/keys/private_key_x25519.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "d": "vsJ1oX5NNi0IGdwGldiac75r-Utmq3Jq4LGv48Q_Qc4", 4 | "crv": "X25519", 5 | "kid": "X25519-01", 6 | "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", 7 | "alg": "ECDH-ES+A128KW" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/private_key_x25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VuBCIEIMAXvyHjAeXy9x4MXF6rwGbDKw7crgDriFTFXO+XsS1F 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/private_key_x448.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "d": "rJJRG3nshyCtd9CgXld8aNaB9YXKR0UOi7zj7hApg9YH4XdBO0G8NcAFNz_uPH2GnCZVcSDgV5c", 4 | "crv": "X448", 5 | "kid": "X448-01", 6 | "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", 7 | "alg": "ECDH-ES+A128KW" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/private_key_x448.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MEYCAQAwBQYDK2VvBDoEOIy3m65HIYDD4b+5sZM0Q8K04GB7LnosbIuCE6gQQn4V 3 | X5w4taqmVXb1bzxQLVocbvuUVyCXD4Gk 4 | -----END PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /tests/keys/public_key_ed25519.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "use": "sig", 4 | "crv": "Ed25519", 5 | "kid": "Ed25519-01", 6 | "x": "2E6dX83gqD_D0eAmqnaHe1TC1xuld6iAKXfw2OVATr0", 7 | "alg": "EdDSA" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/public_key_ed25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEAGEVz4JqD/Q7pS6hu8zlpF/4KMit80XHMh9Lpqeggm9k= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/public_key_ed448.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "use": "sig", 4 | "crv": "Ed448", 5 | "kid": "Ed448-01", 6 | "x": "25isUWIosUkM2ynOPFP5t7BbwM1_iFQmKBpHvA0hgXpRX6yyu-nq6BBmpS3J0DYTlZIoA4qwgSqA", 7 | "alg": "EdDSA" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/public_key_ed448.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MEMwBQYDK2VxAzoAxHNX+MyOuoaolFJxRmnRlBPKd0+/G8b4hjQ/7/WNrd9euo1j 3 | TaA5siU/gVT30viePe6fvLMJqAmA 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /tests/keys/public_key_es256.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "P-256", 5 | "kid": "P-256-01", 6 | "x": "-eZXC6nV-xgthy8zZMCN8pcYSeE2XfWWqckA2fsxHPc", 7 | "y": "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI", 8 | "alg": "ES256" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/public_key_es256.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEp91jKv/CP4v4nDqtzERGnFogBFDv 3 | mQw95iB3MQgmutni2+/+uIoS8jfLFTqKuRo5MEIaGV683N4NuXPBUPOq3Q== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /tests/keys/public_key_es256k.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "secp256k1", 5 | "kid": "secp256k1-01", 6 | "x": "6_W-2tVwr9SU0Ts9sFmZgt8JrlxDi8d6G5ltwL2futU", 7 | "y": "mEOme4oiQsXnNwtlfyH5hEkvLDxrtqDUY0UXBIMRuL0", 8 | "alg": "ES256K" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/public_key_es256k.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEDFjviK+KpOkRsLCmogaO7+5A8dDzoQHm 3 | QYeC4vGcoNo2ABT9EwkH3CwJ2hx9Rw/VEbJI6MkFqB/zUT+jIuw3QQ== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /tests/keys/public_key_es384.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "P-384", 5 | "kid": "P-384-01", 6 | "x": "_XyN9woHaS0mPimSW-etwJMEDSzxIMjp4PjezavU8SHJoClz1bQrcmPb1ZJxHxhI", 7 | "y": "GCNfc32p9sRotx7u2oDGJ3Eqz6q5zPHLdizNn83oRsUTN31eCWfGLHWRury3xF50", 8 | "alg": "ES384" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/public_key_es384.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE7ObQwS3UPiW2D52/4olxQtePuqTgl9CR 3 | zbuQkiyq1BBEwzX+ib9zriwmje/6sMBRyUbUyJfZR7gwWpZFOolVL4l8Y7KdHg5w 4 | 8cTtbJlLOS6IMgYisqXdF0hXHy1yPkZn 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /tests/keys/public_key_es512.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "P-521", 5 | "kid": "P-521-01", 6 | "x": "APkZitSJMJUMB-iPCt47sWu_CrnUHg6IAR4qjmHON-2u41Rjg6DNOS0LZYJJt-AVH5NgGVi8ElIfjo71b9HXCTOc", 7 | "y": "ASx-Cb--149HJ-e1KlSaY-1BOhwOdcTkxSt8BGbW7_hnGfzHsoXM3ywwNcp1Yad-FHUKwmCyMelMQEn2Rh4V2l3I", 8 | "alg": "ES512" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/public_key_es512.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBaVDLKrQEovlkDzB7Cgc+fXxLWoG9 3 | OLNOkLAQ62vSVEJ4UvZsTkWSU4DQa3zZbAQ02GbupktRz6ABP2eHzLTRzpUADaA7 4 | y64fjq3CgsiOJZQs2eJ060ddwLNJ7mPfxQLWY9m0zbhox2w3B3Vgw5ENbCmyBwAQ 5 | oDfNAk4jrD1MkX6isjY= 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /tests/keys/public_key_rsa.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "RSA", 3 | "e": "AQAB", 4 | "use": "sig", 5 | "kid": "RS256-01", 6 | "alg": "RS256", 7 | "n": "o4hWSF-rtXeKI8J32sX3agnJZMyprxQWdmfkaL3jsLK13Yelk7ChlaKIPWU0GHU7Wh_Ce3aslUIb7KsU51Fo3olAVBVszjTYZFu5IIgKtZoni-r8NVHqcmT6JLGhoVGVRaPjEGudmCtxocgiWXGHucl14DChV6PAcPlcbcosNmeJ5z6ia8lp5det-KkjFTe0-EdWB2kTzZvEmbotbtPL-B0BH_jYqmmZWgCU_TZ76QgORiOPmN-GW5RrLOEkay-f-Sx6XGTZxC_heN6eT-Ql1KgGQnagRDOAmcKy4CJp2vmODR8LbOyZl22C8x5wR5Vzz-9NE_WIF0nHT1ivIFOOZQ" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/public_key_rsa.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy9THKaw4pvjiQ1/KRZYK 3 | w/6Fqe8ra3v32Zk2l3wnYGoy9Vh1Rq/7BrFfcKrPGL/GtaDAuSuO7TOWgTzFSI3S 4 | JemdTQpB9hS0qr8yo2bbETDnjMz+9j5L8cpNsM9Sv2EHEZUYcbKBPDGr/WMc2uRp 5 | I8UK7IAiMdIAb+VHv15wF82taoSfBBSUkEmoV2Z1POXfQN6KBMaR0VN/UqP0N9tg 6 | 8BaiD0e2QXWL1/jwgdtJmQiOEFbA2MmjkVBTlCzPsWUKvIEYI1tu/EgjdQBVztPP 7 | lyCBP6GKiMQWh9z5YyNwpRgOSJL9e9m4wdxWoOHLH9WdbVLC7IxC5x7DiJnWveLv 8 | FwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /tests/keys/public_key_x25519.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "crv": "X25519", 4 | "kid": "X25519-01", 5 | "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", 6 | "alg": "ECDH-ES+A128KW" 7 | } 8 | -------------------------------------------------------------------------------- /tests/keys/public_key_x25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VuAyEAoMfvlI5DN08JRFP2fhWvZ6vBEl28yFeS9O9YQUjNyCY= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/public_key_x448.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "crv": "X448", 4 | "kid": "X448-01", 5 | "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", 6 | "alg": "ECDH-ES+A128KW" 7 | } 8 | -------------------------------------------------------------------------------- /tests/keys/public_key_x448.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MEIwBQYDK2VvAzkAz5Ox9snEzH1lzGJh1ewJPgA5+Mx1GwhUqdqX9hxEUZFTUizD 3 | Q1QJd0itKuMIVVfwrrShnl4dcPM= 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /tests/test_algs_aes_key_wrap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for KeyWrap. 3 | """ 4 | 5 | import pytest 6 | 7 | from cwt.algs.symmetric import AESKeyWrap 8 | from cwt.exceptions import DecodeError 9 | 10 | 11 | class TestAESKeyWrap: 12 | """ 13 | Tests for AESKeyWrap. 14 | """ 15 | 16 | def test_aes_key_wrap_constructor_a128kw(self): 17 | key = AESKeyWrap({1: 4, 3: -3}) 18 | assert key.alg == -3 19 | 20 | def test_aes_key_wrap_constructor_a192kw(self): 21 | key = AESKeyWrap({1: 4, 3: -4}) 22 | assert isinstance(key, AESKeyWrap) 23 | assert key.alg == -4 24 | 25 | def test_aes_key_wrap_constructor_a256kw(self): 26 | key = AESKeyWrap({1: 4, 3: -5}) 27 | assert isinstance(key, AESKeyWrap) 28 | assert key.alg == -5 29 | 30 | def test_aes_key_wrap_constructor_with_invalid_alg(self): 31 | with pytest.raises(ValueError) as err: 32 | AESKeyWrap({1: 4, 3: 1}) 33 | pytest.fail("AESKeyWrap() should fail.") 34 | assert "Unknown alg(3) for AES key wrap: 1." in str(err.value) 35 | 36 | def test_aes_key_wrap_constructor_with_invalid_key_ops(self): 37 | with pytest.raises(ValueError) as err: 38 | AESKeyWrap({1: 4, 3: -3, 4: [1, 2]}) 39 | pytest.fail("AESKeyWrap() should fail.") 40 | assert "Unknown or not permissible key_ops(4) for AES key wrap: 1." in str(err.value) 41 | 42 | def test_aes_key_wrap_unwrap_key_with_invalid_alg(self): 43 | key = AESKeyWrap({1: 4, 3: -3}) 44 | with pytest.raises(DecodeError) as err: 45 | key.unwrap_key(b"") 46 | pytest.fail("unwrap_key() should fail.") 47 | assert "Failed to unwrap key." in str(err.value) 48 | -------------------------------------------------------------------------------- /tests/test_algs_raw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for RawKey. 3 | """ 4 | 5 | import pytest 6 | 7 | from cwt.algs.raw import RawKey 8 | 9 | 10 | class TestRawKey: 11 | """ 12 | Tests for RawKey. 13 | """ 14 | 15 | def test_raw_key_constructor(self): 16 | key = RawKey( 17 | { 18 | 1: 4, 19 | -1: b"mysecret", 20 | } 21 | ) 22 | assert key.key == b"mysecret" 23 | assert key.alg is None 24 | assert key.key_ops == [] 25 | assert key.base_iv is None 26 | 27 | @pytest.mark.parametrize( 28 | "invalid, msg", 29 | [ 30 | ( 31 | {1: 2}, 32 | "kty(1) should be Symmetric(4).", 33 | ), 34 | ( 35 | {1: 4}, 36 | "k(-1) should be set.", 37 | ), 38 | ( 39 | {1: 4, -1: 123}, 40 | "k(-1) should be bytes(bstr).", 41 | ), 42 | ], 43 | ) 44 | def test_symmetric_key_constructor_with_invalid_args(self, invalid, msg): 45 | with pytest.raises(ValueError) as err: 46 | RawKey(invalid) 47 | pytest.fail("SymmetricKey should fail.") 48 | assert msg in str(err.value) 49 | 50 | def test_raw_key_to_dict(self): 51 | key = RawKey( 52 | { 53 | 1: 4, 54 | -1: b"mysecret", 55 | } 56 | ) 57 | k = key.to_dict() 58 | assert k[1] == 4 59 | assert k[-1] == b"mysecret" 60 | -------------------------------------------------------------------------------- /tests/test_claims.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0201, R0904, W0621 2 | # R0201: Method could be a function 3 | # R0904: Too many public methods 4 | # W0621: Redefined outer name 5 | 6 | """ 7 | Tests for Claims. 8 | """ 9 | import pytest 10 | 11 | from cwt import Claims 12 | 13 | 14 | class TestClaims: 15 | """ 16 | Tests for Claims. 17 | """ 18 | 19 | def test_claims_constructor(self): 20 | c = Claims({}) 21 | assert isinstance(c, Claims) 22 | 23 | @pytest.mark.parametrize( 24 | "invalid, msg", 25 | [ 26 | ( 27 | {8: "xxx"}, 28 | "cnf(8) should be dict.", 29 | ), 30 | ( 31 | {8: {0: {}}}, 32 | "cnf(8) should include COSE_Key, Encrypted_COSE_Key, or kid.", 33 | ), 34 | ( 35 | {8: {1: "xxx"}}, 36 | "COSE_Key in cnf(8) should be dict.", 37 | ), 38 | ( 39 | {8: {2: "xxx"}}, 40 | "Encrypted_COSE_Key in cnf(8) should be list.", 41 | ), 42 | ( 43 | {8: {3: "xxx"}}, 44 | "kid in cnf(8) should be bytes.", 45 | ), 46 | ], 47 | ) 48 | def test_claims_constructor_with_invalid_arg(self, invalid, msg): 49 | with pytest.raises(ValueError) as err: 50 | Claims(invalid) 51 | pytest.fail("Claims should fail.") 52 | assert msg in str(err.value) 53 | 54 | @pytest.mark.parametrize( 55 | "json, expected", 56 | [ 57 | ({"iss": "coap://as.example.com"}, {1: "coap://as.example.com"}), 58 | ({"sub": "erikw"}, {2: "erikw"}), 59 | ({"aud": "coap://light.example.com"}, {3: "coap://light.example.com"}), 60 | ({"exp": 1444064944}, {4: 1444064944}), 61 | ({"nbf": 1443944944}, {5: 1443944944}), 62 | ({"iat": 1443944944}, {6: 1443944944}), 63 | ({"cti": "123"}, {7: b"123"}), 64 | ({}, {}), 65 | ], 66 | ) 67 | def test_claims_from_json(self, json, expected): 68 | claims = Claims.from_json(json).to_dict() 69 | for k, v in claims.items(): 70 | assert v == expected[k] 71 | assert isinstance(v, type(expected[k])) 72 | assert len(claims) == len(expected) 73 | 74 | @pytest.mark.parametrize( 75 | "json", 76 | [ 77 | { 78 | "iss": "coap://as.example.com", 79 | "sub": "erikw", 80 | "aud": "coap://light.example.com", 81 | "cti": "123", 82 | "exp": 1444064944, 83 | "nbf": 1443944944, 84 | "iat": 1443944944, 85 | "cnf": { 86 | "jwk": { 87 | "kty": "OKP", 88 | "use": "sig", 89 | "crv": "Ed25519", 90 | "kid": "01", 91 | "x": "2E6dX83gqD_D0eAmqnaHe1TC1xuld6iAKXfw2OVATr0", 92 | "alg": "EdDSA", 93 | }, 94 | }, 95 | }, 96 | ], 97 | ) 98 | def test_claims_from_json_with_cnf(self, json): 99 | claims = Claims.from_json(json) 100 | assert claims.iss == "coap://as.example.com" 101 | assert claims.sub == "erikw" 102 | assert claims.aud == "coap://light.example.com" 103 | assert claims.cti == "123" 104 | assert claims.exp == 1444064944 105 | assert claims.nbf == 1443944944 106 | assert claims.iat == 1443944944 107 | assert isinstance(claims.cnf, dict) 108 | 109 | def test_claims_from_json_with_empty_object(self): 110 | claims = Claims.from_json({}) 111 | assert claims.iss is None 112 | assert claims.sub is None 113 | assert claims.aud is None 114 | assert claims.cti is None 115 | assert claims.exp is None 116 | assert claims.nbf is None 117 | assert claims.iat is None 118 | assert claims.cnf is None 119 | 120 | @pytest.mark.parametrize( 121 | "invalid, msg", 122 | [ 123 | ( 124 | {1: "coap://as.example.com"}, 125 | "It is already CBOR-like format.", 126 | ), 127 | ( 128 | {"cnf": "xxx"}, 129 | "cnf value should be dict.", 130 | ), 131 | ( 132 | {"cnf": {"foo": "bar"}}, 133 | "Supported cnf value not found.", 134 | ), 135 | ], 136 | ) 137 | def test_claims_from_json_with_invalid_arg(self, invalid, msg): 138 | with pytest.raises(ValueError) as err: 139 | Claims.from_json(invalid) 140 | pytest.fail("from_json should fail.") 141 | assert msg in str(err.value) 142 | 143 | def test_claims_from_json_with_undefined_key(self): 144 | claims = Claims.from_json( 145 | { 146 | "iss": "coap://as.example.com", 147 | "ext1": "foo", 148 | }, 149 | {"ext": -70001}, 150 | ) 151 | assert claims.get("ext1") is None 152 | 153 | def test_claims_from_json_with_unknown_key(self): 154 | claims = Claims.from_json( 155 | { 156 | "iss": "coap://as.example.com", 157 | "unknown": "something", 158 | } 159 | ).to_dict() 160 | assert len(claims) == 1 161 | assert claims[1] == "coap://as.example.com" 162 | 163 | def test_claims_from_json_with_private_claim_names(self): 164 | claims = Claims.from_json( 165 | { 166 | "iss": "coap://as.example.com", 167 | "ext": "foo", 168 | }, 169 | private_claim_names={"ext": -70001}, 170 | ).to_dict() 171 | assert len(claims) == 2 172 | assert claims[1] == "coap://as.example.com" 173 | assert claims[-70001] == "foo" 174 | 175 | def test_claims_from_json_with_well_known_value(self): 176 | with pytest.raises(ValueError) as err: 177 | Claims.from_json( 178 | { 179 | "iss": "coap://as.example.com", 180 | "ext1": "foo", 181 | }, 182 | private_claim_names={"ext": 1}, 183 | ) 184 | pytest.fail("from_json should fail.") 185 | assert ( 186 | "The claim key should be other than the values listed in https://python-cwt.readthedocs.io/en/stable/claims.html." 187 | in str(err.value) 188 | ) 189 | -------------------------------------------------------------------------------- /tests/test_cose_hpke.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0201, R0904, W0621 2 | # R0201: Method could be a function 3 | # R0904: Too many public methods 4 | # W0621: Redefined outer name 5 | 6 | """ 7 | Tests for COSE. 8 | """ 9 | 10 | import pytest 11 | 12 | from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey 13 | 14 | 15 | class TestCOSE_HPKE: 16 | """ 17 | Tests for COSE-HPKE. 18 | """ 19 | 20 | @pytest.mark.parametrize( 21 | "alg", 22 | [ 23 | COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM, 24 | COSEAlgs.HPKE_BASE_P256_SHA256_CHACHA20POLY1305, 25 | ], 26 | ) 27 | def test_cose_hpke_kem_0x0010(self, alg): 28 | rpk = COSEKey.from_jwk( 29 | { 30 | "kty": "EC", 31 | "kid": "01", 32 | "crv": "P-256", 33 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 34 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 35 | } 36 | ) 37 | 38 | sender = COSE.new() 39 | encoded = sender.encode_and_encrypt( 40 | b"This is the content.", 41 | rpk, 42 | protected={ 43 | COSEHeaders.ALG: alg, 44 | }, 45 | unprotected={ 46 | COSEHeaders.KID: b"01", # kid: "01" 47 | }, 48 | ) 49 | 50 | # The recipient side: 51 | rsk = COSEKey.from_jwk( 52 | { 53 | "kty": "EC", 54 | "kid": "01", 55 | "crv": "P-256", 56 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 57 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 58 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 59 | } 60 | ) 61 | recipient = COSE.new() 62 | assert b"This is the content." == recipient.decode(encoded, rsk) 63 | 64 | @pytest.mark.parametrize( 65 | "alg", 66 | [ 67 | COSEAlgs.HPKE_BASE_P384_SHA384_AES256GCM, 68 | COSEAlgs.HPKE_BASE_P384_SHA384_CHACHA20POLY1305, 69 | ], 70 | ) 71 | def test_cose_hpke_kem_0x0011(self, alg): 72 | rpk = COSEKey.from_jwk( 73 | { 74 | "kty": "EC", 75 | "kid": "01", 76 | "crv": "P-384", 77 | "x": "_XyN9woHaS0mPimSW-etwJMEDSzxIMjp4PjezavU8SHJoClz1bQrcmPb1ZJxHxhI", 78 | "y": "GCNfc32p9sRotx7u2oDGJ3Eqz6q5zPHLdizNn83oRsUTN31eCWfGLHWRury3xF50", 79 | } 80 | ) 81 | 82 | sender = COSE.new() 83 | encoded = sender.encode_and_encrypt( 84 | b"This is the content.", 85 | rpk, 86 | protected={ 87 | COSEHeaders.ALG: alg, 88 | }, 89 | unprotected={ 90 | COSEHeaders.KID: b"01", # kid: "01" 91 | }, 92 | ) 93 | 94 | # The recipient side: 95 | rsk = COSEKey.from_jwk( 96 | { 97 | "kty": "EC", 98 | "kid": "01", 99 | "crv": "P-384", 100 | "x": "_XyN9woHaS0mPimSW-etwJMEDSzxIMjp4PjezavU8SHJoClz1bQrcmPb1ZJxHxhI", 101 | "y": "GCNfc32p9sRotx7u2oDGJ3Eqz6q5zPHLdizNn83oRsUTN31eCWfGLHWRury3xF50", 102 | "d": "1pImEKbrr771-RKi8Tb7tou_WjiR7kwui_nMu16449rk3lzAqf9buUhTkJ-pogkb", 103 | } 104 | ) 105 | recipient = COSE.new() 106 | assert b"This is the content." == recipient.decode(encoded, rsk) 107 | 108 | @pytest.mark.parametrize( 109 | "alg", 110 | [ 111 | COSEAlgs.HPKE_BASE_P521_SHA512_AES256GCM, 112 | COSEAlgs.HPKE_BASE_P521_SHA512_CHACHA20POLY1305, 113 | ], 114 | ) 115 | def test_cose_hpke_kem_0x0012(self, alg): 116 | rpk = COSEKey.from_jwk( 117 | { 118 | "kty": "EC", 119 | "crv": "P-521", 120 | "kid": "01", 121 | "x": "APkZitSJMJUMB-iPCt47sWu_CrnUHg6IAR4qjmHON-2u41Rjg6DNOS0LZYJJt-AVH5NgGVi8ElIfjo71b9HXCTOc", 122 | "y": "ASx-Cb--149HJ-e1KlSaY-1BOhwOdcTkxSt8BGbW7_hnGfzHsoXM3ywwNcp1Yad-FHUKwmCyMelMQEn2Rh4V2l3I", 123 | } 124 | ) 125 | 126 | sender = COSE.new() 127 | encoded = sender.encode_and_encrypt( 128 | b"This is the content.", 129 | rpk, 130 | protected={ 131 | COSEHeaders.ALG: alg, 132 | }, 133 | unprotected={ 134 | COSEHeaders.KID: b"01", # kid: "01" 135 | }, 136 | ) 137 | 138 | # The recipient side: 139 | rsk = COSEKey.from_jwk( 140 | { 141 | "kty": "EC", 142 | "crv": "P-521", 143 | "kid": "01", 144 | "x": "APkZitSJMJUMB-iPCt47sWu_CrnUHg6IAR4qjmHON-2u41Rjg6DNOS0LZYJJt-AVH5NgGVi8ElIfjo71b9HXCTOc", 145 | "y": "ASx-Cb--149HJ-e1KlSaY-1BOhwOdcTkxSt8BGbW7_hnGfzHsoXM3ywwNcp1Yad-FHUKwmCyMelMQEn2Rh4V2l3I", 146 | "d": "ADYyo73ZKicOjwGDYQ_ybZKnVzdAcxGm9OVAxQjzgVM4jaS-Iwtkz90oLdDz3shgKlDgtRK2Aa9lMhqR94hBo4IE", 147 | } 148 | ) 149 | recipient = COSE.new() 150 | assert b"This is the content." == recipient.decode(encoded, rsk) 151 | 152 | @pytest.mark.parametrize( 153 | "alg", 154 | [ 155 | COSEAlgs.HPKE_BASE_X25519_SHA256_AES128GCM, 156 | COSEAlgs.HPKE_BASE_X25519_SHA256_CHACHA20POLY1305, 157 | ], 158 | ) 159 | def test_cose_hpke_kem_0x0020(self, alg): 160 | rpk = COSEKey.from_jwk( 161 | { 162 | "kty": "OKP", 163 | "crv": "X25519", 164 | "kid": "01", 165 | "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", 166 | "key_ops": [], 167 | } 168 | ) 169 | 170 | sender = COSE.new() 171 | encoded = sender.encode_and_encrypt( 172 | b"This is the content.", 173 | rpk, 174 | protected={ 175 | COSEHeaders.ALG: alg, 176 | }, 177 | unprotected={ 178 | COSEHeaders.KID: b"01", # kid: "01" 179 | }, 180 | ) 181 | 182 | # The recipient side: 183 | rsk = COSEKey.from_jwk( 184 | { 185 | "kty": "OKP", 186 | "crv": "X25519", 187 | "kid": "01", 188 | "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", 189 | "d": "vsJ1oX5NNi0IGdwGldiac75r-Utmq3Jq4LGv48Q_Qc4", 190 | "key_ops": ["deriveBits"], 191 | } 192 | ) 193 | recipient = COSE.new() 194 | assert b"This is the content." == recipient.decode(encoded, rsk) 195 | 196 | @pytest.mark.parametrize( 197 | "alg", 198 | [ 199 | COSEAlgs.HPKE_BASE_X448_SHA512_AES256GCM, 200 | COSEAlgs.HPKE_BASE_X448_SHA512_CHACHA20POLY1305, 201 | ], 202 | ) 203 | def test_cose_hpke_kem_0x0021(self, alg): 204 | rpk = COSEKey.from_jwk( 205 | { 206 | "kty": "OKP", 207 | "crv": "X448", 208 | "kid": "01", 209 | "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", 210 | } 211 | ) 212 | 213 | sender = COSE.new() 214 | encoded = sender.encode_and_encrypt( 215 | b"This is the content.", 216 | rpk, 217 | protected={ 218 | COSEHeaders.ALG: alg, 219 | }, 220 | unprotected={ 221 | COSEHeaders.KID: b"01", # kid: "01" 222 | }, 223 | ) 224 | 225 | # The recipient side: 226 | rsk = COSEKey.from_jwk( 227 | { 228 | "kty": "OKP", 229 | "crv": "X448", 230 | "kid": "01", 231 | "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", 232 | "d": "rJJRG3nshyCtd9CgXld8aNaB9YXKR0UOi7zj7hApg9YH4XdBO0G8NcAFNz_uPH2GnCZVcSDgV5c", 233 | "key_ops": ["deriveBits"], 234 | } 235 | ) 236 | recipient = COSE.new() 237 | assert b"This is the content." == recipient.decode(encoded, rsk) 238 | -------------------------------------------------------------------------------- /tests/test_deterministic_encoding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for Core Deterministic Encoding. 3 | """ 4 | 5 | import cbor2 6 | import pytest 7 | 8 | from cwt import COSE, COSEKey 9 | from cwt.const import COSE_ALGORITHMS_MAC, COSE_ALGORITHMS_SIGNATURE 10 | from cwt.utils import sort_keys_for_deterministic_encoding 11 | 12 | 13 | class TestDeterministicEncoding: 14 | """ 15 | Tests for Core Deterministic Encoding 16 | """ 17 | 18 | def test_deterministically_sorted_dict(self): 19 | expected = {} 20 | expected[0] = 0 # Reserved 21 | expected[1] = 0 # alg 22 | expected[4] = 0 # kid 23 | expected[5] = 0 # IV 24 | expected[7] = 0 # counter signature 25 | expected[11] = 0 # Countersignature version 2 26 | expected[12] = 0 # Countersignature0 version 2 27 | expected[15] = 0 # CWT Claims 28 | expected[33] = 0 # x5chain 29 | expected[-1] = 0 # (max of COSE Header Algorithm Parameters resistry) 30 | expected[-65536] = 0 # (min of COSE Header Algorithm Parameters resistry) 31 | expected[-65537] = 0 # (max of Reserved for Private Use) 32 | 33 | d = {} 34 | d[4] = 0 35 | d[-1] = 0 36 | d[33] = 0 37 | d[7] = 0 38 | d[11] = 0 39 | d[1] = 0 40 | d[5] = 0 41 | d[-65537] = 0 42 | d[0] = 0 43 | d[15] = 0 44 | d[12] = 0 45 | d[-65536] = 0 46 | 47 | assert expected == sort_keys_for_deterministic_encoding(d) 48 | 49 | def test_deterministic_cose_sign_binary(self): 50 | sign_jwk = { 51 | "kty": "EC", 52 | "kid": "11", 53 | "alg": "ES256", 54 | "crv": "P-256", 55 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 56 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 57 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 58 | } 59 | sign_key = COSEKey.from_jwk(sign_jwk) 60 | 61 | # create unsorted protected header 62 | p = {} 63 | p["kid"] = sign_jwk["kid"] # 4 64 | p["alg"] = sign_jwk["alg"] # 1 65 | 66 | ctx = COSE.new(deterministic_header=True) 67 | encoded = ctx.encode_and_sign( 68 | payload=b"a", 69 | key=sign_key, 70 | protected=p, 71 | ) 72 | encoded_p = cbor2.loads(encoded).value[0] 73 | 74 | sorted_p = {} 75 | sorted_p[1] = COSE_ALGORITHMS_SIGNATURE[sign_jwk["alg"]] # -7 for ES256 76 | sorted_p[4] = str.encode(sign_jwk["kid"]) # b'11' 77 | expected_p = cbor2.dumps(sorted_p) 78 | 79 | assert expected_p == encoded_p 80 | 81 | @pytest.mark.parametrize( 82 | "alg", 83 | [ 84 | "HMAC 256/64", 85 | "HMAC 256/256", 86 | "HMAC 384/384", 87 | "HMAC 512/512", 88 | ], 89 | ) 90 | def test_deterministic_cose_mac_binary(self, alg): 91 | mac_key = COSEKey.generate_symmetric_key(alg=alg) 92 | 93 | # create unsorted protected header 94 | p = {} 95 | p["kid"] = "01" # 4 96 | p["alg"] = alg # 1 97 | 98 | ctx = COSE.new(deterministic_header=True) 99 | encoded = ctx.encode_and_mac( 100 | payload=b"a", 101 | key=mac_key, 102 | protected=p, 103 | ) 104 | encoded_p = cbor2.loads(encoded).value[0] 105 | 106 | sorted_p = {} 107 | sorted_p[1] = COSE_ALGORITHMS_MAC[alg] 108 | sorted_p[4] = str.encode("01") 109 | expected_p = cbor2.dumps(sorted_p) 110 | 111 | assert expected_p == encoded_p 112 | -------------------------------------------------------------------------------- /tests/test_encrypted_cose_key.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | 3 | import cbor2 4 | import pytest 5 | 6 | from cwt import COSEKey, EncryptedCOSEKey 7 | 8 | 9 | class TestEncryptedCOSEKey: 10 | """ 11 | Tests for EncryptedCOSEKey. 12 | """ 13 | 14 | def test_encrypted_cose_key_from_cose_key_with_nonce(self): 15 | nonce = token_bytes(12) 16 | enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305") 17 | pop_key = COSEKey.from_symmetric_key(alg="HMAC 256/256") 18 | res = EncryptedCOSEKey.from_cose_key(pop_key, enc_key, nonce=nonce) 19 | assert isinstance(res, list) 20 | assert len(res) == 3 21 | protected = cbor2.loads(res[0]) 22 | assert protected[1] == 24 23 | assert isinstance(res[1], dict) 24 | assert isinstance(res[1][5], bytes) and res[1][5] == nonce 25 | 26 | def test_encrypted_cose_key_from_cose_key_with_invalid_encryption_key(self): 27 | enc_key = COSEKey.from_symmetric_key(alg="HMAC 256/64") 28 | pop_key = COSEKey.from_symmetric_key(alg="HMAC 256/256") 29 | with pytest.raises(ValueError) as err: 30 | EncryptedCOSEKey.from_cose_key(pop_key, enc_key) 31 | pytest.fail("to_ should fail.") 32 | assert "Nonce generation is not supported for the key. Set a nonce explicitly." in str(err.value) 33 | -------------------------------------------------------------------------------- /tests/test_helpers_hcert.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cwt import COSEKey, load_pem_hcert_dsc 4 | 5 | 6 | class TestHelperHcert: 7 | def test_helpers_hcert_load_pem_hcert_dsc_es256(self): 8 | dsc = "-----BEGIN CERTIFICATE-----\nMIIBvTCCAWOgAwIBAgIKAXk8i88OleLsuTAKBggqhkjOPQQDAjA2MRYwFAYDVQQDDA1BVCBER0MgQ1NDQSAxMQswCQYDVQQGEwJBVDEPMA0GA1UECgwGQk1TR1BLMB4XDTIxMDUwNTEyNDEwNloXDTIzMDUwNTEyNDEwNlowPTERMA8GA1UEAwwIQVQgRFNDIDExCzAJBgNVBAYTAkFUMQ8wDQYDVQQKDAZCTVNHUEsxCjAIBgNVBAUTATEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASt1Vz1rRuW1HqObUE9MDe7RzIk1gq4XW5GTyHuHTj5cFEn2Rge37+hINfCZZcozpwQKdyaporPUP1TE7UWl0F3o1IwUDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFO49y1ISb6cvXshLcp8UUp9VoGLQMB8GA1UdIwQYMBaAFP7JKEOflGEvef2iMdtopsetwGGeMAoGCCqGSM49BAMCA0gAMEUCIQDG2opotWG8tJXN84ZZqT6wUBz9KF8D+z9NukYvnUEQ3QIgdBLFSTSiDt0UJaDF6St2bkUQuVHW6fQbONd731/M4nc=\n-----END CERTIFICATE-----" 9 | public_key = load_pem_hcert_dsc(dsc) 10 | assert public_key.alg == -7 11 | 12 | def test_helpers_hcert_load_pem_hcert_dsc_ps256(self): 13 | dsc = "-----BEGIN CERTIFICATE-----\nMIIEFzCCAf8CAhI8MA0GCSqGSIb3DQEBBAUAMIGDMQswCQYDVQQGEwJYWDEaMBgGA1UECAwRRXVyb3BlYW4gQ29tbWlzb24xETAPBgNVBAcMCEJydXNzZWxzMQ4wDAYDVQQKDAVESUdJVDEUMBIGA1UECwwLUGVuIFRlc3RpbmcxHzAdBgNVBAMMFlBlbiBUZXN0ZXJzIEFDQyAoQ1NDQSkwHhcNMjEwNTA3MTM0MTE0WhcNMjIwNTA3MTM0MTE0WjAeMQswCQYDVQQGEwJYWDEPMA0GA1UEAwwGUm9iZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqrScBZXVoF4+UcbE8+eRVMsDjvdA9CvxE+f077zYJ6/xaVzNooafTvWI/QHCxNKT8diNoo1uylhvbvCY1sWsjPwCFkTJE49LGm5IetPRHX9zKTsd+fyMj2+yQxx3tkj6d9jO6hmozJhjxSMGvV0IyXIv3fsXHH3kOHpT43mf9MxFBYua4Qxci0RgGYCIJSwZk9jiHRKAFFiDf5hMcqmcezzcMDJc17FhJ7TenqbtzVfr3L9yzZLURqDylVeOit/w5PGyN+KhJRGjPqReqreaFGQsviy3VVf0wKfKMOovHobj7+DYBpyXFuUKE6u0P+3NQfnwwzt6bxLasHwhSmwCCwIDAQABMA0GCSqGSIb3DQEBBAUAA4ICAQBiKvNcpS9aVn58UbUgRLNgOkjhzd5qby/s0bV1XHT7te7rmfoPeB1wypP4XMt6Sfz1hiQcBGTnpbox4/HPkHHGgpJ5RFdFeCUYj4oqIB+MWOqpnQxKIu2f4a2TqgoW2zdvxk9eO9kdoTbxU4xQn/Y6/Wovw29B9nCiMRwa39ZGScDRMQMTbesd/6lJYtSZyjNls2Guxv4kCy3ekFktXQzsUXIrm+Yvhe68+dPgKe26S1xLNRt3kAR30HM5kB3vM8jTGiqubOe6W6YfcX4XoVfmwVfttk2BLPl0u/SXt/SsRHWuYzJ48AUXkK6vd3HR5FG39YSvEZ1Tlf9GRmR2uXO/TnnZiGd+cyjLgAPGtjg1oq0MdzlIFsoFe9cN/XVGjkmRYZ7FzdiSn6IQWUyyoGmFN5B7Q6ZdBMAb58Z3jcTwzmkkHZlfqpUSoK+Hpah515SgjwfY5s9g8vEqefWVmLlYGAiDkfaTYUie53wCXBC+xBJBL7VJnaxqmTKWwM5cRx5uZyOUs6ZQT7CKD1SDk1+C7PAevGKNFTatFn4puITVgQ0NFiIf7ZKOy1w8Zf5aVk0vP3gfOg3SK38RgD81iLTPWr07XTfMZBTaTUr+ph6hxtwSIhHVFsF6n8adl5RynuYDfCCts5E9mOGLqC7ruMKRoOIBOPEwGS5/wIhMO7UEgQ==\n-----END CERTIFICATE-----" 14 | public_key = load_pem_hcert_dsc(dsc) 15 | assert public_key.alg == -37 16 | 17 | def test_helpers_hcert_load_pem_hcert_dsc_ps256_with_bytes(self): 18 | dsc = b"-----BEGIN CERTIFICATE-----\nMIIEFzCCAf8CAhI8MA0GCSqGSIb3DQEBBAUAMIGDMQswCQYDVQQGEwJYWDEaMBgGA1UECAwRRXVyb3BlYW4gQ29tbWlzb24xETAPBgNVBAcMCEJydXNzZWxzMQ4wDAYDVQQKDAVESUdJVDEUMBIGA1UECwwLUGVuIFRlc3RpbmcxHzAdBgNVBAMMFlBlbiBUZXN0ZXJzIEFDQyAoQ1NDQSkwHhcNMjEwNTA3MTM0MTE0WhcNMjIwNTA3MTM0MTE0WjAeMQswCQYDVQQGEwJYWDEPMA0GA1UEAwwGUm9iZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqrScBZXVoF4+UcbE8+eRVMsDjvdA9CvxE+f077zYJ6/xaVzNooafTvWI/QHCxNKT8diNoo1uylhvbvCY1sWsjPwCFkTJE49LGm5IetPRHX9zKTsd+fyMj2+yQxx3tkj6d9jO6hmozJhjxSMGvV0IyXIv3fsXHH3kOHpT43mf9MxFBYua4Qxci0RgGYCIJSwZk9jiHRKAFFiDf5hMcqmcezzcMDJc17FhJ7TenqbtzVfr3L9yzZLURqDylVeOit/w5PGyN+KhJRGjPqReqreaFGQsviy3VVf0wKfKMOovHobj7+DYBpyXFuUKE6u0P+3NQfnwwzt6bxLasHwhSmwCCwIDAQABMA0GCSqGSIb3DQEBBAUAA4ICAQBiKvNcpS9aVn58UbUgRLNgOkjhzd5qby/s0bV1XHT7te7rmfoPeB1wypP4XMt6Sfz1hiQcBGTnpbox4/HPkHHGgpJ5RFdFeCUYj4oqIB+MWOqpnQxKIu2f4a2TqgoW2zdvxk9eO9kdoTbxU4xQn/Y6/Wovw29B9nCiMRwa39ZGScDRMQMTbesd/6lJYtSZyjNls2Guxv4kCy3ekFktXQzsUXIrm+Yvhe68+dPgKe26S1xLNRt3kAR30HM5kB3vM8jTGiqubOe6W6YfcX4XoVfmwVfttk2BLPl0u/SXt/SsRHWuYzJ48AUXkK6vd3HR5FG39YSvEZ1Tlf9GRmR2uXO/TnnZiGd+cyjLgAPGtjg1oq0MdzlIFsoFe9cN/XVGjkmRYZ7FzdiSn6IQWUyyoGmFN5B7Q6ZdBMAb58Z3jcTwzmkkHZlfqpUSoK+Hpah515SgjwfY5s9g8vEqefWVmLlYGAiDkfaTYUie53wCXBC+xBJBL7VJnaxqmTKWwM5cRx5uZyOUs6ZQT7CKD1SDk1+C7PAevGKNFTatFn4puITVgQ0NFiIf7ZKOy1w8Zf5aVk0vP3gfOg3SK38RgD81iLTPWr07XTfMZBTaTUr+ph6hxtwSIhHVFsF6n8adl5RynuYDfCCts5E9mOGLqC7ruMKRoOIBOPEwGS5/wIhMO7UEgQ==\n-----END CERTIFICATE-----" 19 | public_key = load_pem_hcert_dsc(dsc) 20 | assert public_key.alg == -37 21 | 22 | def test_helpers_hcert_load_pem_hcert_dsc_with_invalid_cert(self): 23 | dsc = "xxx" 24 | with pytest.raises(ValueError) as err: 25 | load_pem_hcert_dsc(dsc) 26 | pytest.fail("load_pem_hcert_dsc() should fail.") 27 | assert "Invalid PEM data." in str(err.value) 28 | 29 | def test_helpers_hcert_load_pem_hcert_with_COSEKey_from_pem(self): 30 | dsc = "-----BEGIN CERTIFICATE-----\nMIIEFzCCAf8CAhI8MA0GCSqGSIb3DQEBBAUAMIGDMQswCQYDVQQGEwJYWDEaMBgGA1UECAwRRXVyb3BlYW4gQ29tbWlzb24xETAPBgNVBAcMCEJydXNzZWxzMQ4wDAYDVQQKDAVESUdJVDEUMBIGA1UECwwLUGVuIFRlc3RpbmcxHzAdBgNVBAMMFlBlbiBUZXN0ZXJzIEFDQyAoQ1NDQSkwHhcNMjEwNTA3MTM0MTE0WhcNMjIwNTA3MTM0MTE0WjAeMQswCQYDVQQGEwJYWDEPMA0GA1UEAwwGUm9iZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqrScBZXVoF4+UcbE8+eRVMsDjvdA9CvxE+f077zYJ6/xaVzNooafTvWI/QHCxNKT8diNoo1uylhvbvCY1sWsjPwCFkTJE49LGm5IetPRHX9zKTsd+fyMj2+yQxx3tkj6d9jO6hmozJhjxSMGvV0IyXIv3fsXHH3kOHpT43mf9MxFBYua4Qxci0RgGYCIJSwZk9jiHRKAFFiDf5hMcqmcezzcMDJc17FhJ7TenqbtzVfr3L9yzZLURqDylVeOit/w5PGyN+KhJRGjPqReqreaFGQsviy3VVf0wKfKMOovHobj7+DYBpyXFuUKE6u0P+3NQfnwwzt6bxLasHwhSmwCCwIDAQABMA0GCSqGSIb3DQEBBAUAA4ICAQBiKvNcpS9aVn58UbUgRLNgOkjhzd5qby/s0bV1XHT7te7rmfoPeB1wypP4XMt6Sfz1hiQcBGTnpbox4/HPkHHGgpJ5RFdFeCUYj4oqIB+MWOqpnQxKIu2f4a2TqgoW2zdvxk9eO9kdoTbxU4xQn/Y6/Wovw29B9nCiMRwa39ZGScDRMQMTbesd/6lJYtSZyjNls2Guxv4kCy3ekFktXQzsUXIrm+Yvhe68+dPgKe26S1xLNRt3kAR30HM5kB3vM8jTGiqubOe6W6YfcX4XoVfmwVfttk2BLPl0u/SXt/SsRHWuYzJ48AUXkK6vd3HR5FG39YSvEZ1Tlf9GRmR2uXO/TnnZiGd+cyjLgAPGtjg1oq0MdzlIFsoFe9cN/XVGjkmRYZ7FzdiSn6IQWUyyoGmFN5B7Q6ZdBMAb58Z3jcTwzmkkHZlfqpUSoK+Hpah515SgjwfY5s9g8vEqefWVmLlYGAiDkfaTYUie53wCXBC+xBJBL7VJnaxqmTKWwM5cRx5uZyOUs6ZQT7CKD1SDk1+C7PAevGKNFTatFn4puITVgQ0NFiIf7ZKOy1w8Zf5aVk0vP3gfOg3SK38RgD81iLTPWr07XTfMZBTaTUr+ph6hxtwSIhHVFsF6n8adl5RynuYDfCCts5E9mOGLqC7ruMKRoOIBOPEwGS5/wIhMO7UEgQ==\n-----END CERTIFICATE-----" 31 | public_key = COSEKey.from_pem(dsc, alg="PS256") 32 | assert public_key.alg == -37 33 | 34 | def test_helpers_hcert_load_pem_hcert_dsc_with_unsupported_cert(self): 35 | dsc = "-----BEGIN CERTIFICATE-----\nMIIBBDCBtwIUOcchiKGSdnTvMvPyRR41dCGs7LwwBQYDK2VwMCUxCzAJBgNVBAYTAkpQMRYwFAYDVQQDDA1oY2VydC5leGFtcGxlMB4XDTIxMDcwNTEzMTM1OVoXDTMxMDcwMzEzMTM1OVowJTELMAkGA1UEBhMCSlAxFjAUBgNVBAMMDWhjZXJ0LmV4YW1wbGUwKjAFBgMrZXADIQCF3lYyJUSPHn4Hauiri7/5Jqg807DnrBQk5p0B7Gm/rjAFBgMrZXADQQCWMCmiIWFhfIVw1nZwUZrzeFUps0WOU74WCFKHcxhIHtjr6cJqxdUqjf+wORxUqdqLT3xKrYcWZjqSEYHruJkP\n-----END CERTIFICATE-----" 36 | with pytest.raises(ValueError) as err: 37 | load_pem_hcert_dsc(dsc) 38 | pytest.fail("load_pem_hcert_dsc() should fail.") 39 | assert "Unsupported or unknown key type:" in str(err.value) 40 | -------------------------------------------------------------------------------- /tests/test_key.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0201, R0904, W0621 2 | # R0201: Method could be a function 3 | # R0904: Too many public methods 4 | # W0621: Redefined outer name 5 | 6 | """ 7 | Tests for COSEKeyInterface. 8 | """ 9 | import pytest 10 | 11 | from cwt.cose_key_interface import COSEKeyInterface 12 | 13 | # from secrets import token_bytes 14 | 15 | 16 | # from .utils import key_path 17 | 18 | 19 | class TestCOSEKeyInterface: 20 | """ 21 | Tests for COSEKeyInterface. 22 | """ 23 | 24 | def test_cose_key_constructor(self): 25 | key = COSEKeyInterface({1: 1, 2: b"123"}) 26 | assert key.kty == 1 27 | assert key.kid == b"123" 28 | assert key.key_ops == [] 29 | assert key.base_iv is None 30 | raw = key.to_dict() 31 | assert raw[1] == 1 32 | assert raw[2] == b"123" 33 | with pytest.raises(NotImplementedError): 34 | key.key 35 | pytest.fail("COSEKeyInterface.key should fail.") 36 | with pytest.raises(NotImplementedError): 37 | key.generate_nonce() 38 | pytest.fail("COSEKeyInterface.generate_nonce() should fail.") 39 | with pytest.raises(NotImplementedError): 40 | key.sign(b"message") 41 | pytest.fail("COSEKeyInterface.sign() should fail.") 42 | with pytest.raises(NotImplementedError): 43 | key.verify(b"message", b"signature") 44 | pytest.fail("COSEKeyInterface.verify() should fail.") 45 | with pytest.raises(NotImplementedError): 46 | key.encrypt(b"message", nonce=b"123", aad=None) 47 | pytest.fail("COSEKeyInterface.encrypt() should fail.") 48 | with pytest.raises(NotImplementedError): 49 | key.decrypt(b"message", nonce=b"123", aad=None) 50 | pytest.fail("COSEKeyInterface.decrypt() should fail.") 51 | with pytest.raises(NotImplementedError): 52 | key.wrap_key(b"key_to_wrap") 53 | pytest.fail("COSEKeyInterface.decrypt() should fail.") 54 | with pytest.raises(NotImplementedError): 55 | key.unwrap_key(b"wrapped_key") 56 | pytest.fail("COSEKeyInterface.decrypt() should fail.") 57 | with pytest.raises(NotImplementedError): 58 | key.derive_bytes(16, b"material") 59 | pytest.fail("COSEKeyInterface.derive_bytes() should fail.") 60 | with pytest.raises(NotImplementedError): 61 | key.validate_certificate("/path/to/ca_certs") 62 | pytest.fail("COSEKeyInterface.validate_certificate() should fail.") 63 | 64 | def test_cose_key_constructor_with_alg_and_iv(self): 65 | key = COSEKeyInterface({1: 1, 2: b"123", 3: 1, 5: b"aabbccddee"}) 66 | assert key.base_iv == b"aabbccddee" 67 | raw = key.to_dict() 68 | assert raw[5] == b"aabbccddee" 69 | 70 | def test_cose_key_constructor_without_cose_key(self): 71 | with pytest.raises(TypeError): 72 | COSEKeyInterface() 73 | pytest.fail("COSEKeyInterface should fail.") 74 | 75 | @pytest.mark.parametrize( 76 | "invalid, msg", 77 | [ 78 | ( 79 | {}, 80 | "kty(1) not found.", 81 | ), 82 | ( 83 | {1: b"invalid"}, 84 | "kty(1) should be int or str(tstr).", 85 | ), 86 | ( 87 | {1: {}}, 88 | "kty(1) should be int or str(tstr).", 89 | ), 90 | ( 91 | {1: []}, 92 | "kty(1) should be int or str(tstr).", 93 | ), 94 | ( 95 | {1: "xxx"}, 96 | "Unknown kty: xxx", 97 | ), 98 | ( 99 | {1: 0}, 100 | "Unknown kty: 0", 101 | ), 102 | ( 103 | {1: 1, 2: "123"}, 104 | "kid(2) should be bytes(bstr).", 105 | ), 106 | ( 107 | {1: 1, 2: {}}, 108 | "kid(2) should be bytes(bstr).", 109 | ), 110 | ( 111 | {1: 1, 2: []}, 112 | "kid(2) should be bytes(bstr).", 113 | ), 114 | ( 115 | {1: 1, 2: b"123", 3: b"HMAC 256/256"}, 116 | "alg(3) should be int or str(tstr).", 117 | ), 118 | ( 119 | {1: 1, 2: b"123", 3: {}}, 120 | "alg(3) should be int or str(tstr).", 121 | ), 122 | ( 123 | {1: 1, 2: b"123", 3: []}, 124 | "alg(3) should be int or str(tstr).", 125 | ), 126 | ( 127 | {1: 1, 2: b"123", 3: 1, 4: "sign"}, 128 | "key_ops(4) should be list.", 129 | ), 130 | ( 131 | {1: 1, 2: b"123", 3: 1, 4: b"sign"}, 132 | "key_ops(4) should be list.", 133 | ), 134 | ( 135 | {1: 1, 2: b"123", 3: 1, 4: {}}, 136 | "key_ops(4) should be list.", 137 | ), 138 | ( 139 | {1: 1, 2: b"123", 3: 1, 4: [], 5: "xxx"}, 140 | "Base IV(5) should be bytes(bstr).", 141 | ), 142 | ], 143 | ) 144 | def test_cose_key_constructor_with_invalid_args(self, invalid, msg): 145 | with pytest.raises(ValueError) as err: 146 | COSEKeyInterface(invalid) 147 | pytest.fail("COSEKeyInterface should fail.") 148 | assert msg in str(err.value) 149 | -------------------------------------------------------------------------------- /tests/test_recipient_algs_aes_key_wrap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for KeyWrap. 3 | """ 4 | 5 | from secrets import token_bytes 6 | 7 | import pytest 8 | 9 | from cwt.cose_key import COSEKey 10 | from cwt.exceptions import DecodeError, EncodeError 11 | from cwt.recipient_algs.aes_key_wrap import AESKeyWrap 12 | 13 | 14 | class TestAESKeyWrap: 15 | """ 16 | Tests for AESKeyWrap. 17 | """ 18 | 19 | def test_aes_key_wrap_constructor_a128kw(self): 20 | ctx = AESKeyWrap({1: -3}, sender_key=COSEKey.from_symmetric_key(alg="A128KW")) 21 | assert isinstance(ctx, AESKeyWrap) 22 | assert ctx.alg == -3 23 | 24 | def test_aes_key_wrap_constructor_a192kw(self): 25 | ctx = AESKeyWrap({1: -4}, sender_key=COSEKey.from_symmetric_key(alg="A192KW")) 26 | assert isinstance(ctx, AESKeyWrap) 27 | assert ctx.alg == -4 28 | 29 | def test_aes_key_wrap_constructor_a256kw(self): 30 | ctx = AESKeyWrap({1: -5}, sender_key=COSEKey.from_symmetric_key(alg="A256KW")) 31 | assert isinstance(ctx, AESKeyWrap) 32 | assert ctx.alg == -5 33 | 34 | def test_aes_key_wrap_constructor_a128kw_with_key(self): 35 | ctx = AESKeyWrap( 36 | {1: -3}, 37 | sender_key=COSEKey.from_symmetric_key(alg="A128KW", key=token_bytes(16)), 38 | ) 39 | assert isinstance(ctx, AESKeyWrap) 40 | assert ctx.alg == -3 41 | 42 | def test_aes_key_wrap_constructor_a192kw_with_key(self): 43 | ctx = AESKeyWrap( 44 | {1: -4}, 45 | sender_key=COSEKey.from_symmetric_key(alg="A192KW", key=token_bytes(24)), 46 | ) 47 | assert isinstance(ctx, AESKeyWrap) 48 | assert ctx.alg == -4 49 | 50 | def test_aes_key_wrap_constructor_a256kw_with_key(self): 51 | ctx = AESKeyWrap( 52 | {1: -5}, 53 | sender_key=COSEKey.from_symmetric_key(alg="A256KW", key=token_bytes(32)), 54 | ) 55 | assert isinstance(ctx, AESKeyWrap) 56 | assert ctx.alg == -5 57 | 58 | def test_aes_key_wrap_constructor_without_sender_key(self): 59 | with pytest.raises(ValueError) as err: 60 | AESKeyWrap( 61 | {1: -3}, 62 | ) 63 | pytest.fail("AESKeyWrap() should fail.") 64 | assert "sender_key should be set." in str(err.value) 65 | 66 | def test_aes_key_wrap_constructor_a128kw_with_invalid_key_length(self): 67 | with pytest.raises(ValueError) as err: 68 | AESKeyWrap( 69 | {1: -3}, 70 | sender_key=COSEKey.from_symmetric_key(key="xxx", alg="A128KW"), 71 | ) 72 | pytest.fail("AESKeyWrap() should fail.") 73 | assert "Invalid key length: 3." in str(err.value) 74 | 75 | def test_aes_key_wrap_constructor_a192kw_with_invalid_key_length(self): 76 | with pytest.raises(ValueError) as err: 77 | AESKeyWrap( 78 | {1: -4}, 79 | sender_key=COSEKey.from_symmetric_key(key="xxx", alg="A192KW"), 80 | ) 81 | pytest.fail("AESKeyWrap() should fail.") 82 | assert "Invalid key length: 3." in str(err.value) 83 | 84 | def test_aes_key_wrap_constructor_a256kw_invalid_key_length(self): 85 | with pytest.raises(ValueError) as err: 86 | AESKeyWrap( 87 | {1: -5}, 88 | sender_key=COSEKey.from_symmetric_key(key="xxx", alg="A256KW"), 89 | ) 90 | pytest.fail("AESKeyWrap() should fail.") 91 | assert "Invalid key length: 3." in str(err.value) 92 | 93 | def test_aes_key_wrap_constructor_a128kw_with_invalid_alg_in_sender_key(self): 94 | with pytest.raises(ValueError) as err: 95 | AESKeyWrap( 96 | {1: -3}, 97 | sender_key=COSEKey.from_symmetric_key(alg="A128GCM"), 98 | ) 99 | pytest.fail("AESKeyWrap() should fail.") 100 | assert "Invalid alg in sender_key: 1." in str(err.value) 101 | 102 | def test_aes_key_wrap_constructor_with_invalid_alg(self): 103 | with pytest.raises(ValueError) as err: 104 | AESKeyWrap({1: -1}, sender_key=COSEKey.from_symmetric_key(alg="A128KW")) 105 | pytest.fail("AESKeyWrap() should fail.") 106 | assert "alg in unprotected and sender_key's alg do not match." in str(err.value) 107 | 108 | # def test_aes_key_wrap_encode_with_invalid_key(self): 109 | # key = COSEKey.from_symmetric_key(key="xxx", alg="HS256", kid="01") 110 | # ctx = AESKeyWrap({1: -3}, {}, sender_key=COSEKey.from_symmetric_key(alg="A128KW"), context={"alg": "A128GCM"}) 111 | # with pytest.raises(EncodeError) as err: 112 | # ctx.encode(key) 113 | # pytest.fail("encode() should fail.") 114 | # assert "Failed to wrap key." in str(err.value) 115 | 116 | def test_aes_key_wrap_encode_without_key(self): 117 | ctx = AESKeyWrap({1: -3}, sender_key=COSEKey.from_symmetric_key(alg="A128KW")) 118 | with pytest.raises(EncodeError) as err: 119 | ctx.encode() 120 | pytest.fail("encode() should fail.") 121 | assert "Failed to wrap key." in str(err.value) 122 | 123 | def test_aes_key_wrap_wrap_key_without_alg(self): 124 | enc_key = COSEKey.from_symmetric_key(alg="A128GCM") 125 | key = COSEKey.from_symmetric_key(alg="A128KW", kid="01") 126 | ctx = AESKeyWrap({1: -3}, sender_key=key) 127 | ctx.encode(enc_key.to_bytes()) 128 | with pytest.raises(ValueError) as err: 129 | ctx.decode(key=key, as_cose_key=True) 130 | pytest.fail("decode() should fail.") 131 | assert "alg should be set." in str(err.value) 132 | 133 | def test_aes_key_wrap_wrap_key_without_ciphertext(self): 134 | key = COSEKey.from_symmetric_key(alg="A128GCM", kid="01") 135 | ctx = AESKeyWrap({1: -3}, sender_key=COSEKey.from_symmetric_key(alg="A128KW")) 136 | with pytest.raises(DecodeError) as err: 137 | ctx.decode(key=key, alg="A128GCM", as_cose_key=True) 138 | pytest.fail("decode() should fail.") 139 | assert "Failed to decode key." in str(err.value) 140 | -------------------------------------------------------------------------------- /tests/test_recipient_algs_hpke.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for HPKE. 3 | """ 4 | 5 | import pytest 6 | 7 | from cwt.enums import COSEHeaders 8 | from cwt.recipient_algs.hpke import HPKE 9 | 10 | 11 | class TestHPKE: 12 | """ 13 | Tests for HPKE. 14 | """ 15 | 16 | @pytest.mark.parametrize( 17 | "alg", 18 | [ 19 | 35, 20 | 36, 21 | 37, 22 | 38, 23 | 39, 24 | 40, 25 | 41, 26 | 42, 27 | 43, 28 | 44, 29 | ], 30 | ) 31 | def test_recipient_algs_hpke(self, alg): 32 | ctx = HPKE({COSEHeaders.ALG: alg}, {}) 33 | assert isinstance(ctx, HPKE) 34 | assert ctx.alg == alg 35 | 36 | def test_recipient_algs_hpke_without_alg(self): 37 | with pytest.raises(ValueError) as err: 38 | HPKE({COSEHeaders.ALG: -1}, {}) 39 | pytest.fail("HPKE should fail.") 40 | assert "alg should be one of the HPKE algorithms." in str(err.value) 41 | -------------------------------------------------------------------------------- /tests/test_signer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for Signer. 3 | """ 4 | 5 | import cbor2 6 | import pytest 7 | 8 | from cwt import COSEKey, Signer 9 | 10 | from .utils import key_path 11 | 12 | 13 | class TestSigner: 14 | """ 15 | Tests for Signer. 16 | """ 17 | 18 | def test_signer_constructor(self): 19 | signer = Signer( 20 | cose_key=COSEKey.from_jwk( 21 | { 22 | "kty": "EC", 23 | "kid": "01", 24 | "crv": "P-256", 25 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 26 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 27 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 28 | } 29 | ), 30 | protected={1: -7}, 31 | unprotected={4: b"01"}, 32 | ) 33 | assert signer.unprotected[4] == b"01" 34 | assert cbor2.loads(signer.protected)[1] == -7 35 | assert signer.cose_key.alg == -7 36 | assert signer.cose_key.kid == b"01" 37 | try: 38 | signer.sign(b"Hello world!") 39 | signer.verify(b"Hello world!") 40 | except Exception: 41 | pytest.fail("signer.sign and verify should not fail.") 42 | 43 | def test_signer_constructor_with_protected_bytes(self): 44 | signer = Signer( 45 | cose_key=COSEKey.from_jwk( 46 | { 47 | "kty": "EC", 48 | "kid": "01", 49 | "crv": "P-256", 50 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 51 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 52 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 53 | } 54 | ), 55 | protected=cbor2.dumps({1: -7}), 56 | unprotected={4: b"01"}, 57 | ) 58 | assert signer.unprotected[4] == b"01" 59 | assert cbor2.loads(signer.protected)[1] == -7 60 | assert signer.cose_key.alg == -7 61 | assert signer.cose_key.kid == b"01" 62 | try: 63 | signer.sign(b"Hello world!") 64 | signer.verify(b"Hello world!") 65 | except Exception: 66 | pytest.fail("signer.sign and verify should not fail.") 67 | 68 | def test_signer_new(self): 69 | signer = Signer.new( 70 | cose_key=COSEKey.from_jwk( 71 | { 72 | "kty": "EC", 73 | "kid": "01", 74 | "crv": "P-256", 75 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 76 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 77 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 78 | } 79 | ), 80 | protected={"alg": "ES256"}, 81 | unprotected={"kid": "01"}, 82 | ) 83 | assert signer.unprotected[4] == b"01" 84 | assert cbor2.loads(signer.protected)[1] == -7 85 | assert signer.cose_key.alg == -7 86 | assert signer.cose_key.kid == b"01" 87 | try: 88 | signer.sign(b"Hello world!") 89 | signer.verify(b"Hello world!") 90 | except Exception: 91 | pytest.fail("signer.sign and verify should not fail.") 92 | 93 | def test_signer_new_with_protected_bytes(self): 94 | signer = Signer.new( 95 | cose_key=COSEKey.from_jwk( 96 | { 97 | "kty": "EC", 98 | "kid": "01", 99 | "crv": "P-256", 100 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 101 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 102 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 103 | } 104 | ), 105 | protected=cbor2.dumps({1: -7}), 106 | unprotected={"kid": "01"}, 107 | ) 108 | assert signer.unprotected[4] == b"01" 109 | assert cbor2.loads(signer.protected)[1] == -7 110 | assert signer.cose_key.alg == -7 111 | assert signer.cose_key.kid == b"01" 112 | try: 113 | signer.sign(b"Hello world!") 114 | signer.verify(b"Hello world!") 115 | except Exception: 116 | pytest.fail("signer.sign and verify should not fail.") 117 | 118 | def test_signer_from_jwt(self): 119 | signer = Signer.from_jwk( 120 | { 121 | "kty": "EC", 122 | "kid": "01", 123 | "crv": "P-256", 124 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 125 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 126 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 127 | }, 128 | ) 129 | assert signer.unprotected[4] == b"01" 130 | assert cbor2.loads(signer.protected)[1] == -7 131 | assert signer.cose_key.alg == -7 132 | assert signer.cose_key.kid == b"01" 133 | try: 134 | signer.sign(b"Hello world!") 135 | signer.verify(b"Hello world!") 136 | except Exception: 137 | pytest.fail("signer.sign and verify should not fail.") 138 | 139 | def test_signer_from_jwt_without_kid(self): 140 | signer = Signer.from_jwk( 141 | { 142 | "kty": "EC", 143 | "crv": "P-256", 144 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 145 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 146 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 147 | }, 148 | ) 149 | assert cbor2.loads(signer.protected)[1] == -7 150 | assert signer.cose_key.alg == -7 151 | assert signer.cose_key.kid is None 152 | try: 153 | signer.sign(b"Hello world!") 154 | signer.verify(b"Hello world!") 155 | except Exception: 156 | pytest.fail("signer.sign and verify should not fail.") 157 | 158 | def test_signer_from_jwt_with_key_ops(self): 159 | signer = Signer.from_jwk( 160 | { 161 | "kty": "EC", 162 | "kid": "01", 163 | "crv": "P-256", 164 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 165 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 166 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 167 | "key_ops": ["sign", "verify"], 168 | }, 169 | ) 170 | assert signer.unprotected[4] == b"01" 171 | assert cbor2.loads(signer.protected)[1] == -7 172 | assert signer.cose_key.alg == -7 173 | assert signer.cose_key.kid == b"01" 174 | try: 175 | signer.sign(b"Hello world!") 176 | signer.verify(b"Hello world!") 177 | except Exception: 178 | pytest.fail("signer.sign and verify should not fail.") 179 | 180 | def test_signer_from_jwk_with_invalid_alg(self): 181 | with pytest.raises(ValueError) as err: 182 | Signer.from_jwk({"kty": "oct", "alg": "HS256", "kid": "01", "k": "xxxxxxxxxx"}) 183 | assert "Unsupported or unknown alg for signature: 5." in str(err.value) 184 | 185 | def test_signer_from_jwt_with_invalid_key_ops(self): 186 | with pytest.raises(ValueError) as err: 187 | Signer.from_jwk( 188 | { 189 | "kty": "EC", 190 | "kid": "01", 191 | "crv": "P-256", 192 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 193 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 194 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", 195 | "key_ops": ["encrypt", "decrypt"], 196 | }, 197 | ) 198 | assert "Unknown or not permissible key_ops(4) for EC2." in str(err.value) 199 | 200 | def test_signer_from_pem(self): 201 | with open(key_path("private_key_ed25519.pem")) as key_file: 202 | signer = Signer.from_pem(key_file.read(), kid="01") 203 | assert signer.unprotected[4] == b"01" 204 | assert cbor2.loads(signer.protected)[1] == -8 205 | assert signer.cose_key.alg == -8 206 | assert signer.cose_key.kid == b"01" 207 | try: 208 | signer.sign(b"Hello world!") 209 | signer.verify(b"Hello world!") 210 | except Exception: 211 | pytest.fail("signer.sign and verify should not fail.") 212 | 213 | def test_signer_from_pem_without_kid(self): 214 | with open(key_path("private_key_ed25519.pem")) as key_file: 215 | signer = Signer.from_pem(key_file.read()) 216 | assert cbor2.loads(signer.protected)[1] == -8 217 | assert signer.cose_key.alg == -8 218 | assert signer.cose_key.kid is None 219 | try: 220 | signer.sign(b"Hello world!") 221 | signer.verify(b"Hello world!") 222 | except Exception: 223 | pytest.fail("signer.sign and verify should not fail.") 224 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for utils 3 | """ 4 | 5 | import pytest 6 | 7 | from cwt.utils import base64url_decode, i2osp, to_cis, uint_to_bytes 8 | 9 | 10 | class TestUtils: 11 | """ 12 | Tests for utils. 13 | """ 14 | 15 | def test_utils_i2osp_invalid_arg(self): 16 | with pytest.raises(ValueError) as err: 17 | i2osp(270, 1) 18 | pytest.fail("i2osp should fail.") 19 | assert "integer too large" in str(err.value) 20 | 21 | def test_utils_uint_to_bytes_invalid_arg(self): 22 | with pytest.raises(ValueError) as err: 23 | uint_to_bytes(-1) 24 | pytest.fail("uint_to_bytes should fail.") 25 | assert "Not a positive number." in str(err.value) 26 | 27 | def test_base64url_decode_without_padding(self): 28 | res = base64url_decode("aaaabbbb") 29 | assert len(res) == 6 30 | 31 | def test_to_cis(self): 32 | res = to_cis( 33 | { 34 | "alg": "AES-CCM-16-64-128", 35 | "apu": { 36 | "id": "lighting-client", 37 | "nonce": "aabbccddeeff", 38 | "other": "other PartyV info", 39 | }, 40 | "apv": { 41 | "id": "lighting-server", 42 | "nonce": "112233445566", 43 | "other": "other PartyV info", 44 | }, 45 | "supp_pub": { 46 | "key_data_length": 128, 47 | "protected": {"alg": "direct+HKDF-SHA-256"}, 48 | "other": "Encryption Example 02", 49 | }, 50 | } 51 | ) 52 | assert isinstance(res, list) 53 | assert len(res) == 4 54 | assert res[3][0] == 128 55 | assert res[3][1] == b"\xa1\x01)" 56 | assert res[3][2] == b"Encryption Example 02" 57 | 58 | def test_to_cis_without_supp_pub_other(self): 59 | res = to_cis( 60 | { 61 | "alg": "AES-CCM-16-64-128", 62 | "apu": { 63 | "id": "lighting-client", 64 | "nonce": "aabbccddeeff", 65 | "other": "other PartyV info", 66 | }, 67 | "apv": { 68 | "id": "lighting-server", 69 | "nonce": "112233445566", 70 | "other": "other PartyV info", 71 | }, 72 | "supp_pub": { 73 | "key_data_length": 128, 74 | "protected": {"alg": "direct+HKDF-SHA-256"}, 75 | }, 76 | } 77 | ) 78 | assert isinstance(res, list) 79 | 80 | def test_to_cis_without_apu_apv_supp_pub_other(self): 81 | res = to_cis( 82 | { 83 | "alg": "A128KW", 84 | "supp_pub": { 85 | "key_data_length": 128, 86 | "protected": {"alg": "ECDH-ES+A128KW"}, 87 | "other": "SUIT Payload Encryption", 88 | }, 89 | } 90 | ) 91 | assert isinstance(res, list) 92 | 93 | @pytest.mark.parametrize( 94 | "invalid, msg", 95 | [ 96 | ( 97 | {}, 98 | "alg not found.", 99 | ), 100 | ( 101 | {"alg": "xxx"}, 102 | "Unsupported or unknown alg for context information: xxx.", 103 | ), 104 | ( 105 | {"alg": "AES-CCM-16-64-128", "apu": 123}, 106 | "apu should be dict.", 107 | ), 108 | ( 109 | {"alg": "AES-CCM-16-64-128", "apu": {"id": 123}}, 110 | "apu.id should be str.", 111 | ), 112 | ( 113 | {"alg": "AES-CCM-16-64-128", "apu": {"nonce": []}}, 114 | "apu.nonce should be str or int.", 115 | ), 116 | ( 117 | {"alg": "AES-CCM-16-64-128", "apu": {"nonce": 123, "other": 123}}, 118 | "apu.other should be str.", 119 | ), 120 | ( 121 | {"alg": "AES-CCM-16-64-128", "apv": 123}, 122 | "apv should be dict.", 123 | ), 124 | ( 125 | {"alg": "AES-CCM-16-64-128", "apv": {"id": 123}}, 126 | "apv.id should be str.", 127 | ), 128 | ( 129 | {"alg": "AES-CCM-16-64-128", "apv": {"nonce": []}}, 130 | "apv.nonce should be str or int.", 131 | ), 132 | ( 133 | {"alg": "AES-CCM-16-64-128", "apv": {"nonce": 123, "other": 123}}, 134 | "apv.other should be str.", 135 | ), 136 | ( 137 | {"alg": "AES-CCM-16-64-128", "supp_pub": 123}, 138 | "supp_pub should be dict.", 139 | ), 140 | ( 141 | {"alg": "AES-CCM-16-64-128", "supp_pub": {"key_data_length": "xxx"}}, 142 | "supp_pub.key_data_length should be int.", 143 | ), 144 | ( 145 | {"alg": "AES-CCM-16-64-128", "supp_pub": {"protected": "xxx"}}, 146 | "supp_pub.protected should be dict.", 147 | ), 148 | ( 149 | {"alg": "AES-CCM-16-64-128", "supp_pub": {"other": 123}}, 150 | "supp_pub.other should be str.", 151 | ), 152 | ], 153 | ) 154 | def test_to_cis_with_invalid_args(self, invalid, msg): 155 | with pytest.raises(ValueError) as err: 156 | to_cis(invalid) 157 | pytest.fail("cis should fail.") 158 | assert msg in str(err.value) 159 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from calendar import timegm 3 | from datetime import datetime 4 | 5 | 6 | def now() -> int: 7 | return timegm(datetime.utcnow().utctimetuple()) 8 | 9 | 10 | def key_path(key_name: str) -> str: 11 | return os.path.join(os.path.dirname(os.path.realpath(__file__)), "keys", key_name) 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | check 4 | build 5 | build_docs 6 | py{39,310,311,312, 313} 7 | isolated_build = True 8 | skip_missing_interpreters = True 9 | 10 | 11 | [gh-actions] 12 | python = 13 | 3.9: py39 14 | 3.10: py310 15 | 3.11: py311 16 | 3.12: check, build, build_docs, py312 17 | 3.13: py313 18 | 19 | 20 | [testenv:check] 21 | allowlist_externals = poetry 22 | skip_install = true 23 | commands = 24 | poetry install --no-root --only main,dev 25 | poetry run pre-commit run --all-files 26 | 27 | 28 | [testenv:build] 29 | allowlist_externals = poetry 30 | skip_install = true 31 | commands = 32 | poetry build 33 | 34 | 35 | [testenv:build_docs] 36 | allowlist_externals = poetry 37 | skip_install = true 38 | commands = 39 | poetry install --only main,docs 40 | sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html 41 | 42 | 43 | [testenv] 44 | allowlist_externals = poetry 45 | skip_install = true 46 | commands = 47 | poetry install --only main,dev 48 | poetry run pytest -ra --cov=cwt --cov-report=term --cov-report=xml tests 49 | --------------------------------------------------------------------------------