├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── iotics-identity-tag.yml │ └── iotics-identity.yml ├── .gitignore ├── .pylint.rc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── flake8.cfg ├── how_to ├── README.md ├── high_level_api.py ├── high_level_api_code_only.py ├── regular_api.py └── regular_api_code_only.py ├── iotics ├── __init__.py └── lib │ ├── __init__.py │ └── identity │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── advanced_api.py │ ├── advanced_api_keys.py │ ├── advanced_api_proof.py │ ├── high_level_api.py │ └── regular_api.py │ ├── const.py │ ├── crypto │ ├── __init__.py │ ├── identity.py │ ├── issuer.py │ ├── jwt.py │ ├── key_pair_secrets.py │ ├── keys.py │ └── proof.py │ ├── error.py │ ├── register │ ├── __init__.py │ ├── document.py │ ├── document_builder.py │ ├── document_helper.py │ ├── key_pair.py │ ├── keys.py │ ├── resolver.py │ └── rest_resolver.py │ └── validation │ ├── __init__.py │ ├── authentication.py │ ├── document.py │ ├── identity.py │ └── proof.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── behaviour │ ├── common.py │ ├── features │ │ ├── advanced_identity_api.feature │ │ ├── high_level_identity_api.feature │ │ └── identity_api.feature │ ├── test_advanced_identity.py │ ├── test_high_level_identity_api.py │ └── test_identity.py └── unit │ ├── __init__.py │ └── iotics │ ├── __init__.py │ └── lib │ ├── __init__.py │ └── identity │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── test_advanced_api.py │ ├── test_advanced_api_proof.py │ └── test_high_level_api.py │ ├── conftest.py │ ├── crypto │ ├── __init__.py │ ├── conftest.py │ ├── test_identity.py │ ├── test_issuer.py │ ├── test_jwt.py │ ├── test_key_pair_helpers.py │ ├── test_key_pair_validation.py │ ├── test_keys.py │ └── test_proof.py │ ├── fake.py │ ├── helper.py │ ├── register │ ├── __init__.py │ ├── conftest.py │ ├── test_document.py │ ├── test_document_builder.py │ ├── test_document_helper.py │ ├── test_keys.py │ └── test_rest_resolver.py │ └── validation │ ├── __init__.py │ ├── helper.py │ ├── test_authentication.py │ ├── test_document.py │ ├── test_identifier.py │ └── test_proof.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Steps to reproduce** 11 | _[Please provide definite, as clear as possible steps, preferably with test data. Include tool and library names used (if any) and their versions.]_ 12 | 13 | 14 | **Expected result** 15 | … 16 | 17 | 18 | **Actual result** 19 | … 20 | 21 | 22 | **How often does it happen?** 23 | 24 | 25 | **Timestamps:** 26 | _[Exact date and time or range of when it happened - including timezone.]_ 27 | 28 | 29 | **Affected hosts/spaces:** 30 | … 31 | 32 | 33 | **What is impact of the issue:** 34 | _[Additional information to help to understand severity of the bug.]_ 35 | 36 | 37 | **Additional context:** 38 | _[Please attach Screenshots, scripts, related logs (files, not inline code) from monitoring, stacktraces, terminal output, etc.]_ 39 | 40 | 41 | Please link any related issues. 42 | -------------------------------------------------------------------------------- /.github/workflows/iotics-identity-tag.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | name: Iotics Identity Tag Repo (Manual) 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | tag-repo: 9 | name: Tag Iotics Identity Git Repository 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3.9 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.9 17 | - name: Tag the git repo 18 | run: | 19 | pip install . 20 | TAG=$(python -c 'from importlib.metadata import version; print(version("iotics-identity"))') 21 | git tag $TAG 22 | git push origin $TAG 23 | -------------------------------------------------------------------------------- /.github/workflows/iotics-identity.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | name: Iotics Identity 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | release: 10 | types: 11 | - created 12 | 13 | jobs: 14 | validate: 15 | name: Validate library on "${{ matrix.os }}" with python:${{ matrix.python-version }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macOS-latest] 20 | python-version: ["3.8", "3.9", "3.10", "3.11"] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip tox 31 | - name: Lint with flake8/pylint/mypy 32 | run: | 33 | tox -e lint 34 | - name: Test with pytest 35 | run: | 36 | tox -e pytest 37 | - name: Test with pytest BDD 38 | run: | 39 | tox -e pytestbdd 40 | package: 41 | name: Package the library 42 | runs-on: ubuntu-latest 43 | needs: validate 44 | steps: 45 | - uses: actions/checkout@v2 46 | - name: Set up Python 3.8 47 | uses: actions/setup-python@v2 48 | with: 49 | python-version: '3.8' 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip wheel==0.40.0 53 | - name: Package 54 | run: | 55 | python3 setup.py -q clean -a 56 | python3 setup.py sdist bdist_wheel 57 | 58 | - uses: actions/upload-artifact@master 59 | with: 60 | name: pkgs 61 | path: dist/ 62 | 63 | check-pkgs: 64 | name: Validate the packages on "${{ matrix.os }}" with python:${{ matrix.python-version }} 65 | runs-on: ${{ matrix.os }} 66 | needs: package 67 | strategy: 68 | matrix: 69 | os: [ubuntu-latest, windows-latest, macOS-latest] 70 | python-version: ["3.8", "3.9", "3.10", "3.11"] 71 | 72 | steps: 73 | - uses: actions/checkout@v2 74 | - name: Set up Python ${{ matrix.python-version }} 75 | uses: actions/setup-python@v2 76 | with: 77 | python-version: ${{ matrix.python-version }} 78 | - name: Install dependencies 79 | run: | 80 | python -m pip install --upgrade pip 81 | - uses: actions/download-artifact@master 82 | with: 83 | name: pkgs 84 | path: . 85 | - name: Check source package 86 | run: | 87 | pip install iotics-identity-*.tar.gz 88 | python -c 'import iotics.lib.identity' 89 | pip uninstall -y iotics-identity 90 | shell: bash 91 | - name: Check wheel package 92 | run: | 93 | pip install iotics_identity-*.whl 94 | python -c 'import iotics.lib.identity' 95 | pip uninstall -y iotics-identity 96 | shell: bash 97 | 98 | deploy: 99 | runs-on: ubuntu-latest 100 | needs: check-pkgs 101 | steps: 102 | - uses: actions/download-artifact@master 103 | with: 104 | name: pkgs 105 | path: ./dist 106 | - name: Set up Python 107 | uses: actions/setup-python@v2 108 | with: 109 | python-version: '3.8' 110 | - name: Publish package to TestPyPI 111 | uses: pypa/gh-action-pypi-publish@release/v1 112 | with: 113 | user: __token__ 114 | password: ${{ secrets.TEST_PYPI_PASSWORD }} 115 | repository_url: https://test.pypi.org/legacy/ 116 | packages_dir: ./dist/ 117 | verify_metadata: true 118 | verbose: true 119 | skip_existing: true 120 | 121 | - name: Publish package to PyPi 122 | if: github.event_name == 'release' && github.event.action == 'created' 123 | uses: pypa/gh-action-pypi-publish@release/v1 124 | with: 125 | user: __token__ 126 | password: ${{ secrets.PYPI_PASSWORD2024 }} 127 | packages_dir: ./dist/ 128 | verify_metadata: true 129 | verbose: true 130 | skip_existing: false 131 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | .vscode 5 | .idea 6 | ubjson/*.html 7 | *.py[cod] 8 | *.so 9 | .coverage 10 | coverage.xml 11 | coverage 12 | 13 | *.egg 14 | .eggs/ 15 | *.egg-info 16 | 17 | env 18 | venv 19 | .env 20 | .venv 21 | 22 | # mypy 23 | .mypy_cache/ 24 | 25 | 26 | # Unit test / coverage reports 27 | htmlcov/ 28 | .tox/ 29 | .nox/ 30 | .coverage 31 | .coverage.* 32 | .cache 33 | test-bdd-results.xml 34 | test-lib-results.xml 35 | test-unit-results.xml 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Iotics Identity SDK in Python 2 | 3 | `iotics-identity-py` is an open source project and accepts contributions. 4 | 5 | ## Becoming a contributor 6 | 7 | The first step is to configure your environment. 8 | 9 | ## Before contributing code 10 | 11 | The project welcomes submissions, but to make sure things are well coordinated we ask that contributors discuss any significant changes before starting work. Best practice is to connect your work to the [issue tracker](https://github.com/Iotic-Labs/iotics-identity-py/issues), either by filing a new issue or by claiming an existing issue. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iotics-identity-py 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/iotics-identity)](https://pypi.org/project/iotics-identity) 4 | [![PyPI downloads](https://img.shields.io/pypi/dm/iotics-identity)](https://pypi.org/project/iotics-identity/#files) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-yellow.svg)](https://github.com/Iotic-Labs/iotics-identity-py/blob/main/LICENSE) 6 | [![GitHub Issues](https://img.shields.io/github/issues/Iotic-Labs/iotics-identity-py)](https://github.com/Iotic-Labs/iotics-identity-py/issues) 7 | [![GitHub Contributors](https://img.shields.io/github/contributors/Iotic-Labs/iotics-identity-py)](https://github.com/Iotic-Labs/iotics-identity-py) 8 | 9 | Create Data Mesh. Use interoperable digital twins to create data interactions and build powerful real-time data products. This repository is a library for Decentralised Identity (DID) management with Iotics for applications in Python v3.8+. 10 | 11 | You need to have an IOTICSpace to take advantage of this DID SDK. Contact product@iotics.com for a free trial or [![sign up](https://img.shields.io/badge/sign%20up-164194.svg?style=flat)](https://www.iotics.com/signup-preview-program/) 12 | 13 | ## Introduction to Iotics 14 | 15 | Interoperate any data, digital twin or service across legacy, on-prem, cloud, IoT, and analytical technologies creating a secure decentralised, federated network of interactions. 16 | 17 | Power long-term digital transformation using real-time business event streams. Unlock the power of your business by eliminating complex infrastructure and shortening time-to-value. 18 | 19 | To learn more about IOTICS see our [website](https://www.iotics.com/) or [documentation site](https://docs.iotics.com). 20 | 21 | ## Identity API 22 | 23 | The identity API is used to manage identities and authentication in the Iotics Host. 24 | The API is split in 3 level according to the user needs: 25 | 26 | * [High level identity API](https://github.com/Iotic-Labs/iotics-identity-py/tree/main/iotics/lib/identity/api/high_level_api.py): minimal set of features to interact with Iotics Host 27 | * [Identity API](https://github.com/Iotic-Labs/iotics-identity-py/tree/main/iotics/lib/identity/api/regular_api.py): set of features for basic identities management 28 | * [Advanced identity API](https://github.com/Iotic-Labs/iotics-identity-py/tree/main/iotics/lib/identity/api/advanced_api.py): set of features for advanced identities management 29 | 30 | ## How to 31 | 32 | Two examples are provided to illustrate the usage of the **high level API** and the **regular api**. 33 | See [Iotics Identity API How To](https://github.com/Iotic-Labs/iotics-identity-py/tree/main/how_to/README.md). 34 | 35 | You can also follow these tutorials on [docs.iotics.com](https://docs.iotics.com/docs/create-decentralized-identity-documents). 36 | 37 | * Setup your dev environment: \ 38 | `pip install -e '.[dev]'` 39 | 40 | * Run the linter: \ 41 | `tox -e lint` 42 | 43 | * Run type analysis: \ 44 | `tox -e mypy` 45 | 46 | * Run unit tests: \ 47 | `tox -e pytest` 48 | 49 | * Run BDD tests: \ 50 | `tox -e pytestbdd` 51 | 52 | ## Reporting issues 53 | 54 | The issue tracker for this project is currently located at [GitHub](https://github.com/Iotic-Labs/iotics-identity-py/issues). 55 | 56 | Please report any issues there with a sufficient description of the bug or feature request. Bug reports should ideally be accompanied by a minimal reproduction of the issue. Irreproducible bugs are difficult to diagnose and fix (and likely to be closed after some period of time). 57 | 58 | Bug reports must specify the version of the `iotics-identity-py` module. 59 | 60 | ## Contributing 61 | 62 | This project is open-source and accepts contributions. See the [contribution guide](https://github.com/Iotic-Labs/iotics-identity-py/tree/main/CONTRIBUTING.md) for more information. 63 | 64 | ## License 65 | 66 | Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/Iotic-Labs/iotics-identity-py/tree/main/LICENSE) in the project root for license information. 67 | 68 | ## Technology Used 69 | 70 | * Markdown 71 | * Python 72 | * pylint 73 | * pytest 74 | * mypy 75 | * Tox 76 | * DID 77 | * BDD 78 | -------------------------------------------------------------------------------- /flake8.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # comma-separated filename and glob patterns default: .svn,CVS,.bzr,.hg,.git,__pycache 3 | #exclude = 4 | # comma-separated filename and glob patterns default: *.py 5 | #filename = 6 | # select errors and warnings to enable which are off by default 7 | # select = 8 | # skip errors or warnings 9 | ignore = 10 | # do not use bare except 11 | E722 12 | # line break after binary operator 13 | W504 14 | # set maximum allowed line length default: 79 15 | max-line-length = 120 16 | # set the error format 17 | #format = 18 | # McCabe complexity threshold 19 | max-complexity = 16 20 | -------------------------------------------------------------------------------- /how_to/README.md: -------------------------------------------------------------------------------- 1 | Iotics Identity API How To 2 | ========================== 3 | 4 | # High level Key concepts (not exhaustive) 5 | An **Identity** is represented by a pair of secrets used to generate private and public keys. 6 | 7 | A **pair of secrets** is composed of a secret seed (see how to generate below) and a key name (a string). 8 | ``` 9 | # Install iotics.lib.identity 10 | 11 | # How to generate a seed using the code 12 | [HighLevelIdentityApi or IdentityApi].create_seed() 13 | 14 | # How to generate a see using the command line 15 | iotics-identity-create-seed 16 | ``` 17 | 18 | A **Registered Identity** of type TWIN, AGENT or USER is created from an **Identity** (or **pair of secrets**). 19 | When a **Registered Identity** is created a **Register document** associated to this identity is created and registered 20 | against the **Resolver**. The **Registered Identity** owns the document. 21 | 22 | A **Registered Identity** can (not exhaustive): 23 | - Add new owner (an **Identity** public key) to a **Register Document** the **Registered Identity** owns. 24 | - Allow an other **Identity** (using its public key) to authenticate using the **Register Document** the **Registered Identity** owns. 25 | - Delegate Authentication to an **other Registered Identity** on a **Register Document** the **Registered Identity** owns. The 26 | **other Registered Identity** can authentication on behalf of the **Registered Identity**. 27 | - Delegate Control to an **other Registered Identity** on a **Register Document** the **Registered Identity** owns. The 28 | **other Registered Identity** can control the **Register Document** the **Registered Identity** owns. 29 | 30 | # Minimal requirements to interact with Iotics Host 31 | 32 | 1. Authenticate with a token required by the Iotics Web API: 33 | - Create a USER Registered Identity. 34 | - Create an AGENT Registered Identity. 35 | - Setup an Authentication delegation from the USER to the AGENT (USER delegates to the AGENT so AGENT can work on behalf of the USER). 36 | - Create an authentication token. 37 | - Use the token in the Iotics Web API headers. 38 | 39 | 2. Create Iotics Twins using the Iotics Web API: 40 | - Create a TWIN Registered Identity. 41 | - Setup a Control delegation from the TWIN to the AGENT (TWIN delegates to the AGENT so AGENT can control the TWIN). 42 | - Use the twin decentralised identifier (registered_identity.did) to create the Iotics Twin using the Iotics Web API. 43 | 44 | See the **High Level Identity API** and/or **Identity API** sections below to easily comply with those requirements and 45 | start using Iotics. 46 | 47 | # High level Identity API 48 | 49 | Minimal set of features to interact with Iotics Host. 50 | 51 | Key features: 52 | - **Create USER and AGENT identities with authentication delegation**: set of identities required to authenticate against Iotics Host 53 | - **Create an AGENT authentication token with duration**: token required by the Iotics Host API for authentication 54 | - **Create TWIN identity with control delegation**: create a Twin identity to enable the creation and update of an Iotics Twin 55 | 56 | ### Try it 57 | > To run this example you will need a resolver url and to set the following environment variable: 58 | > 59 | > export RESOLVER=[resolver url] 60 | 61 | See scripts: [How to use the high level identity API](./high_level_api.py) 62 | 63 | Run it and have a look at the output 64 | ```bash 65 | export RESOLVER='http://localhost:5000' 66 | pip install iotics-identity 67 | python ./high_level_api.py 68 | ``` 69 | 70 | See the ["code only" version of the "how to use the high level identity API"](./high_level_api_code_only.py) to 71 | have a look at the code usage without noise (without print). 72 | 73 | 74 | # Identity API 75 | 76 | Set of features for basic identities management. 77 | 78 | Key features: 79 | - **Create a user identity**: needed to authenticate against the Iotics host 80 | - **Create an agent identity**: needed to authenticate against the Iotics host 81 | - **User identity delegates Authentication to the Agent identity**: Agent can authenticate on behalf of USer against the Iotics host. 82 | - **Create an AGENT authentication token with duration**: token required by the Iotics Host API for authentication 83 | - **Create a twin identity**: create a Twin identity to enable the creation of an Iotics Twin 84 | - **Twin delegates Control to Agent**: enable Agent to control (update/delete/...) an Iotics Twin 85 | 86 | 87 | ### Try it 88 | > To run this example you will need a resolver url and to set the following environment variable: 89 | > 90 | > export RESOLVER=[resolver url] 91 | 92 | See scripts: [How to use the identity API](./regular_api.py) 93 | 94 | Run it and have a look at the output 95 | ```bash 96 | export RESOLVER='http://localhost:5000' 97 | pip install iotics-identity 98 | python ./regular_api.py 99 | ``` 100 | 101 | See the ["code only" version of the "how to use the identity API"](./regular_api_code_only.py) to 102 | have a look at the code usage without noise (without print). 103 | -------------------------------------------------------------------------------- /how_to/high_level_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iotics.lib.identity.api.high_level_api import get_rest_high_level_identity_api 4 | 5 | RESOLVER_URL = os.environ.get('RESOLVER') 6 | if not RESOLVER_URL: 7 | print('Missing RESOLVER url environment variable') 8 | exit(1) 9 | print() 10 | print('# High level Identity API example') 11 | print('In this example each identity secrets (seeds and key names) are generated. The user is in charge to' 12 | 'save those secrets to keep control of the identities') 13 | print() 14 | print('Features:') 15 | print('\t 1. create USER and AGENT identities with authentication delegation') 16 | print('\t 2. AGENT token generation for interaction with Iotics Host') 17 | print('\t 3. create TWIN identity with control delegation') 18 | print() 19 | print() 20 | api = get_rest_high_level_identity_api(resolver_url=RESOLVER_URL) 21 | 22 | print('## 1. Create USER and AGENT identities with authentication delegation so the AGENT can authenticate ' 23 | 'on behalf of the USER') 24 | user_seed, user_key_name, user_name = api.create_seed(), '#MyUserKey', '#MyUserName' 25 | agent_seed, agent_key_name, agent_name = api.create_seed(), '#MyAgentKey', '#MyAgentName' 26 | 27 | user_registered_id, agent_registered_id = api.create_user_and_agent_with_auth_delegation(user_seed=user_seed, 28 | user_key_name=user_key_name, 29 | user_name=user_name, 30 | agent_seed=agent_seed, 31 | agent_key_name=agent_key_name, 32 | agent_name=agent_name, 33 | delegation_name='#AuthDeleg') 34 | print(f'User and agent identities have been created with authentication delegation') 35 | print('For this example the following secrets have been generated:') 36 | print() 37 | print(f'Created USER identity: {user_registered_id.issuer}') 38 | print(f'\t name: {user_name}') 39 | print(f'\t key name: {user_key_name}') 40 | print(f'\t seed: {user_seed.hex()}') 41 | print() 42 | print(f'Created AGENT identity: {agent_registered_id.issuer}') 43 | print(f'\t name: {agent_name}') 44 | print(f'\t key name: {agent_key_name}') 45 | print(f'\t seed: {agent_seed.hex()}') 46 | print() 47 | print('> The api user is responsible to save the secrets to keep control of its own identity') 48 | 49 | print() 50 | print() 51 | print('## 2. Create agent authentication token') 52 | token = api.create_agent_auth_token(agent_registered_identity=agent_registered_id, 53 | user_did=user_registered_id.did, 54 | duration=3600) # seconds 55 | print('Token has been generated and can be used to interact with the Iotics host') 56 | print(f'token: \'{token}\'') 57 | 58 | print() 59 | print() 60 | print('## 3. Create a twin with control delegation to the AGENT so the AGENT can control the twin') 61 | twin_seed, twin_key_name, twin_name = api.create_seed(), '#MyTwin1Key', '#MyTwin1Name' 62 | twin_registered_id = api.create_twin_with_control_delegation(twin_seed=twin_seed, 63 | twin_key_name=twin_key_name, 64 | twin_name=twin_name, 65 | agent_registered_identity=agent_registered_id, 66 | delegation_name='#ControlDeleg') 67 | print(f'Twin identity has been created with control delegation nto the AGENT') 68 | print('For this example the following secrets have been generated:') 69 | print(f'Created TWIN identity: {twin_registered_id.issuer}') 70 | print() 71 | print(f'\t name: {twin_name}') 72 | print(f'\t key name: {twin_key_name}') 73 | print(f'\t seed: {twin_seed.hex()}') 74 | 75 | print() 76 | print('> The api user is responsible to save the secrets to keep control of its own identity') 77 | -------------------------------------------------------------------------------- /how_to/high_level_api_code_only.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iotics.lib.identity.api.high_level_api import get_rest_high_level_identity_api 4 | 5 | RESOLVER_URL = os.environ.get('RESOLVER') 6 | 7 | api = get_rest_high_level_identity_api(resolver_url=RESOLVER_URL) 8 | 9 | # -- Create User and Agent with Authentication delegation --------------------------------------------------------- # 10 | user_seed, user_key_name, user_name = api.create_seed(), '#MyUserKey', '#MyUserName' 11 | agent_seed, agent_key_name, agent_name = api.create_seed(), '#MyAgentKey', '#MyAgentName' 12 | 13 | user_registered_id, agent_registered_id = api.create_user_and_agent_with_auth_delegation(user_seed=user_seed, 14 | user_key_name=user_key_name, 15 | user_name=user_name, 16 | agent_seed=agent_seed, 17 | agent_key_name=agent_key_name, 18 | agent_name=agent_name, 19 | delegation_name='#AuthDeleg') 20 | # -- Create token -------------------------------------------------------------------------------------------------- # 21 | 22 | token = api.create_agent_auth_token(agent_registered_identity=agent_registered_id, 23 | user_did=user_registered_id.did, 24 | duration=3600) # seconds 25 | 26 | # -- Create Twin with Control delegation --------------------------------------------------------------------------- # 27 | twin_seed, twin_key_name, twin_name = api.create_seed(), '#MyTwin1Key', '#MyTwin1Name' 28 | twin_registered_id = api.create_twin_with_control_delegation(twin_seed=twin_seed, 29 | twin_key_name=twin_key_name, 30 | twin_name=twin_name, 31 | agent_registered_identity=agent_registered_id, 32 | delegation_name='#ControlDeleg') 33 | -------------------------------------------------------------------------------- /how_to/regular_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from builtins import print 3 | from pprint import pprint 4 | 5 | from iotics.lib.identity.api.regular_api import get_rest_identity_api 6 | 7 | RESOLVER_URL = os.environ.get('RESOLVER') 8 | if not RESOLVER_URL: 9 | print('Missing RESOLVER url environment variable') 10 | exit(1) 11 | print() 12 | print('# Identity API example') 13 | print('In this example each identity secrets (seeds and key names) are generated. The user is in charge to' 14 | 'save those secrets to keep control of the identities') 15 | print() 16 | print('Features:') 17 | print('\t 1. create USER identity') 18 | print('\t 2. create AGENT identity') 19 | print('\t 3. USER delegates authentication to AGENT') 20 | print('\t 4. AGENT token generation for interaction with Iotics Host') 21 | print('\t 5. create TWIN identity ') 22 | print('\t 6. TWIN delegates control to AGENT') 23 | print() 24 | print() 25 | api = get_rest_identity_api(resolver_url=RESOLVER_URL) 26 | print('## 1. Create USER identity') 27 | user_seed, user_key_name, user_name = api.create_seed(), '#MyUserKey', '#MyUserName' 28 | user_registered_id = api.create_user_identity(user_seed=user_seed, 29 | user_key_name=user_key_name, 30 | user_name=user_name) 31 | print(f'USER has been created') 32 | print('For this example the following secrets have been generated:') 33 | print() 34 | print(f'Created USER identity: {user_registered_id.issuer}') 35 | print(f'\t name: {user_name}') 36 | print(f'\t key name: {user_key_name}') 37 | print(f'\t seed: {user_seed.hex()}') 38 | print() 39 | print('> The api user is responsible to save the secrets to keep control of its own identity') 40 | print('USER document:') 41 | pprint(api.get_register_document(user_registered_id.did).to_dict()) 42 | 43 | print() 44 | print() 45 | print('## 2. Create AGENT identity') 46 | agent_seed, agent_key_name, agent_name = api.create_seed(), '#MyAgentKey', '#MyAgentName' 47 | agent_registered_id = api.create_agent_identity(agent_seed=agent_seed, 48 | agent_key_name=agent_key_name, 49 | agent_name=agent_name) 50 | print(f'AGENT has been created') 51 | print('For this example the following secrets have been generated:') 52 | print() 53 | print(f'Created AGENT identity: {agent_registered_id.issuer}') 54 | print(f'\t name: {agent_name}') 55 | print(f'\t key name: {agent_key_name}') 56 | print(f'\t seed: {agent_seed.hex()}') 57 | print() 58 | print('> The api user is responsible to save the secrets to keep control of its own identity') 59 | print('AGENT document:') 60 | pprint(api.get_register_document(agent_registered_id.did).to_dict()) 61 | 62 | print() 63 | print() 64 | print('## 3. USER delegates authentication to the AGENT so the AGENT can authenticate on behalf of the USER') 65 | api.user_delegates_authentication_to_agent(user_registered_identity=user_registered_id, 66 | agent_registered_identity=agent_registered_id, 67 | delegation_name='#AuthDeleg') 68 | 69 | print(f'USER authentication delegation has been added to the USER document. Now the AGENT can create a token to ' 70 | f'authenticate on behalf on the agent') 71 | print('USER document:') 72 | pprint(api.get_register_document(user_registered_id.did).to_dict()) 73 | 74 | print() 75 | print() 76 | print('## 4. Create agent authentication token') 77 | token = api.create_agent_auth_token(agent_registered_identity=agent_registered_id, 78 | user_did=user_registered_id.did, 79 | duration=3600) # seconds 80 | print('Token has been generated and can be used to interact with the Iotics host') 81 | print(f'token: \'{token}\'') 82 | 83 | print() 84 | print() 85 | print('## 5. Create TWIN identity') 86 | twin_seed, twin_key_name, twin_name = api.create_seed(), '#MyTwinKey', '#MyTwinName' 87 | twin_registered_id = api.create_twin_identity(twin_seed=twin_seed, 88 | twin_key_name=twin_key_name, 89 | twin_name=twin_name) 90 | print(f'TWIN has been created') 91 | print('For this example the following secrets have been generated:') 92 | print() 93 | print(f'Created TWIN identity: {twin_registered_id.issuer}') 94 | print(f'\t name: {twin_name}') 95 | print(f'\t key name: {twin_key_name}') 96 | print(f'\t seed: {twin_seed.hex()}') 97 | print() 98 | print('> The api user is responsible to save the secrets to keep control of its own identity') 99 | print('TWIN document:') 100 | pprint(api.get_register_document(twin_registered_id.did).to_dict()) 101 | 102 | print() 103 | print() 104 | print('## 6. TWIN delegates control to the AGENT so the AGENT can control the twin') 105 | api.twin_delegates_control_to_agent(twin_registered_identity=twin_registered_id, 106 | agent_registered_identity=agent_registered_id, 107 | delegation_name='#ControlDeleg') 108 | 109 | print(f'TWIN control delegation has been added to the TWIN document. Now the AGENT can control the twin ' 110 | f'(Iotics create/update/...') 111 | print('TWIN document:') 112 | pprint(api.get_register_document(twin_registered_id.did).to_dict()) 113 | -------------------------------------------------------------------------------- /how_to/regular_api_code_only.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iotics.lib.identity.api.regular_api import get_rest_identity_api 4 | 5 | RESOLVER_URL = os.environ.get('RESOLVER') 6 | api = get_rest_identity_api(resolver_url=RESOLVER_URL) 7 | 8 | # -- Create User ---------------------------------------------------------------------------------------------------- # 9 | user_seed, user_key_name, user_name = api.create_seed(), '#MyUserKey', '#MyUserName' 10 | user_registered_id = api.create_user_identity(user_seed=user_seed, 11 | user_key_name=user_key_name, 12 | user_name=user_name) 13 | 14 | # -- Create Agent --------------------------------------------------------------------------------------------------- # 15 | agent_seed, agent_key_name, agent_name = api.create_seed(), '#MyAgentKey', '#MyAgentName' 16 | agent_registered_id = api.create_agent_identity(agent_seed=agent_seed, 17 | agent_key_name=agent_key_name, 18 | agent_name=agent_name) 19 | 20 | # -- Delegate Authentication ---------------------------------------------------------------------------------------- # 21 | api.user_delegates_authentication_to_agent(user_registered_identity=user_registered_id, 22 | agent_registered_identity=agent_registered_id, 23 | delegation_name='#AuthDeleg') 24 | 25 | # -- Create token --------------------------------------------------------------------------------------------------- # 26 | token = api.create_agent_auth_token(agent_registered_identity=agent_registered_id, 27 | user_did=user_registered_id.did, 28 | duration=3600) # seconds 29 | 30 | # -- Create Twin ---------------------------------------------------------------------------------------------------- # 31 | twin_seed, twin_key_name, twin_name = api.create_seed(), '#MyTwinKey', '#MyTwinName' 32 | twin_registered_id = api.create_twin_identity(twin_seed=twin_seed, 33 | twin_key_name=twin_key_name, 34 | twin_name=twin_name) 35 | 36 | # -- Delegate Control ----------------------------------------------------------------------------------------------- # 37 | api.twin_delegates_control_to_agent(twin_registered_identity=twin_registered_id, 38 | agent_registered_identity=agent_registered_id, 39 | delegation_name='#ControlDeleg') 40 | 41 | -------------------------------------------------------------------------------- /iotics/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | # For pkgutil namespace compatibility only. Must NOT contain anything else. See also: 3 | # https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages 4 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore 5 | -------------------------------------------------------------------------------- /iotics/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | # For pkgutil namespace compatibility only. Must NOT contain anything else. See also: 3 | # https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages 4 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore 5 | -------------------------------------------------------------------------------- /iotics/lib/identity/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from iotics.lib.identity.api import * # noqa: F403,F401 4 | from iotics.lib.identity.error import * # noqa: F403,F401 5 | from iotics.lib.identity.register.resolver import * # noqa: F403,F401 6 | from iotics.lib.identity.register.rest_resolver import * # noqa: F403,F401 7 | -------------------------------------------------------------------------------- /iotics/lib/identity/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | from iotics.lib.identity.api.advanced_api import * # noqa: F403,F401 3 | from iotics.lib.identity.api.high_level_api import * # noqa: F403,F401 4 | from iotics.lib.identity.api.regular_api import * # noqa: F403,F401 5 | -------------------------------------------------------------------------------- /iotics/lib/identity/api/advanced_api_keys.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | from abc import ABC, abstractmethod 3 | from typing import Optional 4 | 5 | from iotics.lib.identity.error import IdentityRegisterDocumentKeyNotFoundError 6 | from iotics.lib.identity.register.document import RegisterDocument 7 | from iotics.lib.identity.register.document_builder import RegisterDocumentBuilder 8 | from iotics.lib.identity.register.keys import RegisterAuthenticationPublicKey, RegisterDelegationProof, \ 9 | RegisterKeyBase, RegisterPublicKey 10 | 11 | 12 | class RegisterDocKeyApi(ABC): 13 | 14 | @abstractmethod 15 | def add_key_to_builder(self, builder: RegisterDocumentBuilder, key: RegisterKeyBase): 16 | """ 17 | Add a new register key to the register document builder 18 | :param builder: register document builder 19 | :param key: register key base 20 | :raises: 21 | IdentityRegisterDocumentKeyConflictError: if key name is not unique 22 | """ 23 | raise NotImplementedError 24 | 25 | @abstractmethod 26 | def get_key_from_doc(self, doc: RegisterDocument, key_name: str) -> Optional[RegisterKeyBase]: 27 | """ 28 | Get a register key from the document 29 | :param doc: register document 30 | :param key_name: key name 31 | :return: optional associated register key 32 | """ 33 | raise NotImplementedError 34 | 35 | def add_doc_key(self, doc: RegisterDocument, key: RegisterKeyBase) -> RegisterDocument: 36 | """ 37 | Add a new register key to the register document 38 | :param doc: register document 39 | :param key: register key base 40 | 41 | :raises: 42 | IdentityInvalidDocumentError: if invalid document 43 | IdentityRegisterDocumentKeyConflictError: if key name is not unique 44 | """ 45 | builder = RegisterDocumentBuilder() 46 | self.add_key_to_builder(builder, key) 47 | return builder.build_from_existing(doc) 48 | 49 | @staticmethod 50 | def remove_doc_key(doc: RegisterDocument, key_name: str) -> RegisterDocument: 51 | """ 52 | Remove a register key from a register document 53 | :param doc: register document 54 | :param key_name: register key name 55 | :return: 56 | """ 57 | return RegisterDocumentBuilder() \ 58 | .set_keys_from_existing(doc) \ 59 | .remove_key(key_name) \ 60 | .build_from_existing(doc, populate_with_doc_keys=False) 61 | 62 | def revoke_doc_key(self, doc: RegisterDocument, key_name: str, revoked: bool) -> RegisterDocument: 63 | """ 64 | Create a new document setting revoked to the key associated to the key name 65 | :param doc: a register document 66 | :param key_name: a key name 67 | :param revoked: is the key revoked 68 | :return: a register document 69 | 70 | :raises: 71 | - IdentityRegisterDocumentKeyNotFoundError: if the key to revoke is not found 72 | 73 | """ 74 | key = self.get_key_from_doc(doc, key_name) 75 | if not key: 76 | raise IdentityRegisterDocumentKeyNotFoundError(f'Can mot revoke key {key_name} fron document {doc.did}:' 77 | f'key not found') 78 | builder = RegisterDocumentBuilder() \ 79 | .set_keys_from_existing(doc) \ 80 | .remove_key(key_name) 81 | self.add_key_to_builder(builder, key.get_new_key(revoked)) 82 | return builder.build_from_existing(doc, populate_with_doc_keys=False) 83 | 84 | 85 | class RegisterPublicDocKeysApi(RegisterDocKeyApi): 86 | 87 | def get_key_from_doc(self, doc: RegisterDocument, key_name: str) -> Optional[RegisterKeyBase]: 88 | return doc.public_keys.get(key_name) 89 | 90 | def add_key_to_builder(self, builder: RegisterDocumentBuilder, key: RegisterPublicKey): # type: ignore 91 | builder.add_public_key_obj(key) 92 | 93 | 94 | class RegisterAuthPublicDocKeysApi(RegisterDocKeyApi): 95 | def get_key_from_doc(self, doc: RegisterDocument, key_name: str) -> Optional[RegisterKeyBase]: 96 | return doc.auth_keys.get(key_name) 97 | 98 | def add_key_to_builder(self, builder: RegisterDocumentBuilder, # type: ignore 99 | key: RegisterAuthenticationPublicKey): 100 | builder.add_authentication_key_obj(key) 101 | 102 | 103 | class RegisterCtrlDelegPublicDocKeysApi(RegisterDocKeyApi): 104 | 105 | def get_key_from_doc(self, doc: RegisterDocument, key_name: str) -> Optional[RegisterKeyBase]: 106 | return doc.control_delegation_proof.get(key_name) 107 | 108 | def add_key_to_builder(self, builder: RegisterDocumentBuilder, key: RegisterDelegationProof): # type: ignore 109 | builder.add_control_delegation_obj(key) 110 | 111 | 112 | class RegisterAuthDelegPublicDocKeysApi(RegisterDocKeyApi): 113 | 114 | def get_key_from_doc(self, doc: RegisterDocument, key_name: str) -> Optional[RegisterKeyBase]: 115 | return doc.auth_delegation_proof.get(key_name) 116 | 117 | def add_key_to_builder(self, builder: RegisterDocumentBuilder, key: RegisterDelegationProof): # type: ignore 118 | builder.add_authentication_delegation_obj(key) 119 | -------------------------------------------------------------------------------- /iotics/lib/identity/api/advanced_api_proof.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | from dataclasses import dataclass 3 | from typing import Union 4 | 5 | from iotics.lib.identity.crypto.proof import Issuer, KeyPairSecrets, Proof 6 | from iotics.lib.identity.register.keys import DelegationProofType 7 | 8 | 9 | @dataclass(frozen=True) 10 | class APIProof: 11 | issuer: Issuer 12 | content: bytes 13 | signature: str 14 | 15 | @property 16 | def p_type(self) -> DelegationProofType: 17 | return DelegationProofType.DID 18 | 19 | @staticmethod 20 | def build(key_pair: KeyPairSecrets, issuer: Issuer, content: bytes) -> 'APIProof': 21 | proof = Proof.build(key_pair, issuer, content) 22 | return APIProof(proof.issuer, proof.content, proof.signature) 23 | 24 | 25 | @dataclass(frozen=True) 26 | class APIDidDelegationProof(APIProof): 27 | 28 | @property 29 | def p_type(self) -> DelegationProofType: 30 | return DelegationProofType.DID 31 | 32 | # pylint: disable=arguments-renamed 33 | @staticmethod 34 | def build(key_pair: KeyPairSecrets, issuer: Issuer, did: str) -> 'APIDidDelegationProof': # type: ignore 35 | proof = APIProof.build(key_pair, issuer, did.encode()) 36 | return APIDidDelegationProof(proof.issuer, proof.content, proof.signature) 37 | 38 | 39 | @dataclass(frozen=True) 40 | class APIGenericDelegationProof(APIProof): 41 | 42 | @property 43 | def p_type(self) -> DelegationProofType: 44 | return DelegationProofType.GENERIC 45 | 46 | # pylint: disable=arguments-differ 47 | @staticmethod 48 | def build(key_pair: KeyPairSecrets, issuer: Issuer) -> 'APIGenericDelegationProof': # type: ignore 49 | proof = APIProof.build(key_pair, issuer, b'') 50 | return APIGenericDelegationProof(proof.issuer, proof.content, proof.signature) 51 | 52 | 53 | def get_proof_type(proof: Union[Proof, APIProof]) -> DelegationProofType: 54 | """ 55 | Backward compatibility function to allow legacy Proof usage and new API Delegation Proof usage 56 | :param proof: a Proof or an API Delegation Proof 57 | :return: the proof type or the default proof type if not present 58 | """ 59 | return getattr(proof, 'p_type', DelegationProofType.DID) 60 | -------------------------------------------------------------------------------- /iotics/lib/identity/const.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | # Secrets validation 4 | 5 | MIN_SEED_METHOD_NONE_LEN = 16 6 | KEY_PAIR_PATH_PREFIX = 'iotics/0' 7 | 8 | # Identifier Validation 9 | NAME_PATTERN_RAW = r'[a-zA-Z\-\_0-9]{1,24}' 10 | ISSUER_SEPARATOR = '#' 11 | IDENTIFIER_PREFIX = 'did:iotics:' 12 | IDENTIFIER_ID = fr'{IDENTIFIER_PREFIX}iot(?P[a-km-zA-HJ-NP-Z1-9]{{33}})' 13 | IDENTIFIER_ID_PATTERN = fr'^{IDENTIFIER_ID}$' 14 | IDENTIFIER_NAME_PATTERN = fr'^\{ISSUER_SEPARATOR}{NAME_PATTERN_RAW}$' 15 | ISSUER_PATTERN = fr'^{IDENTIFIER_ID}\#{NAME_PATTERN_RAW}' 16 | 17 | # Document constants 18 | DOCUMENT_CONTEXT = 'https://w3id.org/did/v1' 19 | DOCUMENT_VERSION = '0.0.1' 20 | SUPPORTED_VERSIONS = ('0.0.0', DOCUMENT_VERSION) 21 | # public key type string 22 | DOCUMENT_PUBLIC_KEY_TYPE = 'Secp256k1VerificationKey2018' 23 | # authentication public key type string 24 | DOCUMENT_AUTHENTICATION_TYPE = 'Secp256k1SignatureAuthentication2018' 25 | # Document metadata validation 26 | DOCUMENT_MAX_LABEL_LENGTH = 64 27 | DOCUMENT_MAX_COMMENT_LENGTH = 512 28 | DOCUMENT_MAX_URL_LENGTH = 512 29 | 30 | # Token constants 31 | # Default offset for token valid-from time used. This is to avoid tokens being rejected when the client time is 32 | # marginally ahead of the server (i.e. resolver or Iotics host). 33 | DEFAULT_TOKEN_START_OFFSET_SECONDS = -30 34 | TOKEN_ALGORITHM = 'ES256' 35 | -------------------------------------------------------------------------------- /iotics/lib/identity/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /iotics/lib/identity/crypto/identity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from _blake2 import blake2b # type: ignore 4 | 5 | import base58 6 | 7 | from iotics.lib.identity.const import IDENTIFIER_PREFIX, ISSUER_SEPARATOR 8 | 9 | 10 | def is_same_identifier(issuer_a: str, issuer_b: str) -> bool: 11 | """ 12 | check if 2 issuers string have the same identifier 13 | :param issuer_a: issuer string a 14 | :param issuer_b: issuer string b 15 | :return: True if identifier are the same else False 16 | """ 17 | return issuer_a.split(ISSUER_SEPARATOR)[0] == issuer_b.split(ISSUER_SEPARATOR)[0] 18 | 19 | 20 | IDENTIFIER_METHOD = 0x05 21 | IDENTIFIER_VERSION = 0x55 22 | IDENTIFIER_PAD = 0x59 23 | 24 | 25 | def make_identifier(public_bytes: bytes) -> str: 26 | """ 27 | Generate a new decentralised identifier from public key as bytes 28 | :param public_bytes: public key as bytes 29 | :return: decentralised identifier 30 | """ 31 | bl2 = blake2b(digest_size=20) 32 | bl2.update(public_bytes) 33 | pk_digest = bl2.digest() 34 | 35 | cl2 = blake2b(digest_size=20) 36 | cl2.update(pk_digest) 37 | checksum = bytearray.fromhex(cl2.hexdigest())[:4] 38 | 39 | return IDENTIFIER_PREFIX + base58.b58encode(bytes([IDENTIFIER_METHOD, IDENTIFIER_VERSION, IDENTIFIER_PAD]) 40 | + pk_digest + checksum).decode('ascii') 41 | -------------------------------------------------------------------------------- /iotics/lib/identity/crypto/issuer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from dataclasses import dataclass 4 | 5 | from iotics.lib.identity.const import ISSUER_SEPARATOR 6 | from iotics.lib.identity.error import IdentityValidationError 7 | from iotics.lib.identity.validation.identity import IdentityValidation 8 | 9 | 10 | @dataclass(frozen=True) 11 | class Issuer: 12 | did: str 13 | name: str 14 | 15 | @staticmethod 16 | def build(did: str, name: str) -> 'Issuer': 17 | """ 18 | Build a valid issuer. 19 | :param did: issuer decentralised identifier 20 | :param name: issuer name 21 | :return: valid issuer 22 | 23 | :raises: 24 | IdentityValidationError: if invalid name or did 25 | 26 | """ 27 | IdentityValidation.validate_identifier(did) 28 | IdentityValidation.validate_key_name(name) 29 | return Issuer(did, name) 30 | 31 | @staticmethod 32 | def from_string(issuer_string: str) -> 'Issuer': 33 | """ 34 | Build a valid issuer from issuer string. 35 | :param issuer_string: issuer string 36 | :return: valid issuer 37 | 38 | :raises: 39 | IdentityValidationError: if invalid issuer string 40 | """ 41 | parts = issuer_string.split(ISSUER_SEPARATOR) 42 | if len(parts) != 2: 43 | raise IdentityValidationError( 44 | f'Invalid issuer string {issuer_string} should be of the form of [did]{ISSUER_SEPARATOR}[name]') 45 | return Issuer.build(parts[0], f'{ISSUER_SEPARATOR}{parts[1]}') 46 | 47 | def __str__(self) -> str: 48 | return f'{self.did}{self.name}' 49 | 50 | 51 | @dataclass(frozen=True) 52 | class IssuerKey: 53 | issuer: Issuer 54 | public_key_base58: str 55 | 56 | @staticmethod 57 | def build(did: str, name: str, public_key_base58: str) -> 'IssuerKey': 58 | """ 59 | Build an issuer key from identifier, name and public key. 60 | :param did: issuer decentralised identifier 61 | :param name: issuer name 62 | :param public_key_base58: public key base58 63 | :return: issuer key with valid issuer 64 | 65 | :raises: 66 | IdentityValidationError: if invalid name or did 67 | """ 68 | return IssuerKey(Issuer.build(did, name), public_key_base58) 69 | -------------------------------------------------------------------------------- /iotics/lib/identity/crypto/jwt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from datetime import datetime 4 | 5 | import jwt 6 | from cryptography.hazmat.primitives.asymmetric import ec 7 | 8 | from iotics.lib.identity.const import DEFAULT_TOKEN_START_OFFSET_SECONDS, TOKEN_ALGORITHM 9 | from iotics.lib.identity.crypto.issuer import Issuer 10 | from iotics.lib.identity.crypto.keys import KeysHelper 11 | from iotics.lib.identity.error import IdentityValidationError 12 | from iotics.lib.identity.register.document import RegisterDocument 13 | 14 | 15 | class JwtTokenHelper: 16 | 17 | @staticmethod 18 | def decode_token(token: str) -> dict: 19 | """ 20 | Decode a jwt token without verifying it. 21 | :param token: jwt token 22 | :return: decoded token 23 | 24 | :raises: 25 | IdentityValidationError: if invalid token 26 | """ 27 | try: 28 | return jwt.decode(token, options={'verify_signature': False}, 29 | algorithms=[TOKEN_ALGORITHM], verify=False) 30 | except jwt.exceptions.DecodeError as err: 31 | raise IdentityValidationError(f'Can not decode invalid token: \'{err}\'') from err 32 | 33 | @staticmethod 34 | def decode_and_verify_token(token: str, public_base58: str, audience: str): 35 | """ 36 | Decode a jwt token and verifying it. 37 | :param token: jwt token 38 | :param public_base58: token public base58 key 39 | :param audience: token audience 40 | :return: decoded verified token 41 | 42 | :raises: 43 | IdentityValidationError: if invalid token 44 | IdentityValidationError: if invalid token signature 45 | IdentityValidationError: if expired token 46 | """ 47 | try: 48 | key = KeysHelper.get_public_ECDSA_from_base58(public_base58) 49 | return jwt.decode(token, key, audience=audience, algorithms=[TOKEN_ALGORITHM], # type: ignore 50 | verify=True, options={'verify_signature': True, 'verify_aud': bool(audience)}) 51 | except jwt.exceptions.InvalidSignatureError as err: 52 | raise IdentityValidationError(f'Invalid token signature: \'{err}\'') from err 53 | except jwt.exceptions.ExpiredSignatureError as err: 54 | raise IdentityValidationError(f'Expired token: \'{err}\'') from err 55 | except jwt.exceptions.DecodeError as err: 56 | raise IdentityValidationError(f'Can not decode invalid token: \'{err}\'') from err 57 | 58 | @staticmethod 59 | def create_doc_token(issuer: Issuer, audience: str, doc: RegisterDocument, 60 | private_key: ec.EllipticCurvePrivateKey) -> str: 61 | """ 62 | Create a register document jwt token. 63 | :param issuer: document issuer 64 | :param audience: token audience 65 | :param doc: register document 66 | :param private_key: issuer private key 67 | :return: encoded jwt token 68 | 69 | :raises: 70 | IdentityValidationError: if can not encode the token 71 | """ 72 | try: 73 | return jwt.encode({'iss': str(issuer), 'aud': audience, 'doc': doc.to_dict()}, private_key, # type: ignore 74 | algorithm=TOKEN_ALGORITHM) 75 | except (TypeError, ValueError) as err: 76 | raise IdentityValidationError(f'Can not create document token for {issuer}: \'{err}\'') from err 77 | 78 | @staticmethod 79 | def create_auth_token(iss: str, sub: str, aud: str, duration: int, 80 | private_key: ec.EllipticCurvePrivateKey, 81 | start_offset: int = DEFAULT_TOKEN_START_OFFSET_SECONDS) -> str: 82 | """ 83 | Create an authentication jwt token. 84 | :param iss: issuer as string 85 | :param sub: subject document did 86 | :param aud: token audience 87 | :param duration: token duration (seconds) 88 | :param private_key: issuer private key 89 | :param start_offset: offset for token valid-from time used (default=DEFAULT_TOKEN_START_OFFSET_SECONDS) 90 | :return: encoded jwt token 91 | 92 | :raises: 93 | IdentityValidationError: if invalid duration (<=0) 94 | IdentityValidationError: if can not encode the token 95 | """ 96 | now = int(datetime.now().timestamp()) 97 | if duration < 0: 98 | raise IdentityValidationError(f'Can not create auth token with duration={duration}, must be >0') 99 | try: 100 | return jwt.encode({ 101 | 'iss': iss, 102 | 'aud': aud, 103 | 'sub': sub, 104 | 'iat': now + start_offset, 105 | 'exp': now + duration 106 | }, private_key, algorithm='ES256') # type: ignore 107 | except (TypeError, ValueError) as err: 108 | raise IdentityValidationError(f'Can not create auth token for {iss}/{sub}: \'{err}\'') from err 109 | -------------------------------------------------------------------------------- /iotics/lib/identity/crypto/keys.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from dataclasses import dataclass 4 | from typing import Tuple 5 | 6 | import base58 7 | from cryptography.hazmat.backends import default_backend 8 | from cryptography.hazmat.primitives import serialization 9 | from cryptography.hazmat.primitives.asymmetric import ec 10 | 11 | from iotics.lib.identity.error import IdentityDependencyError, IdentityValidationError 12 | 13 | 14 | @dataclass(frozen=True) 15 | class KeyPair: 16 | private_key: ec.EllipticCurvePrivateKey 17 | public_bytes: bytes 18 | public_base58: str 19 | 20 | 21 | class KeysHelper: 22 | 23 | @staticmethod 24 | def get_private_ECDSA(private_expo: str) -> ec.EllipticCurvePrivateKey: 25 | """ 26 | Get private key (ECDSA) from private exponent 27 | :param private_expo: private exponent as hex string 28 | :return: private ECDSA key 29 | 30 | :raises: 31 | IdentityDependencyError: if incompatible EllipticCurve dependency 32 | """ 33 | sbin = bytes.fromhex(private_expo) 34 | sint = int.from_bytes(sbin, 'big', signed=False) 35 | 36 | try: 37 | return ec.derive_private_key(sint, ec.SECP256K1(), default_backend()) 38 | except Exception as err: 39 | raise IdentityDependencyError(f'Dependency cryptography failed to derive private key: {err}') from err 40 | 41 | @staticmethod 42 | def get_public_keys_from_private_ECDSA(private_key: ec.EllipticCurvePrivateKey) -> Tuple[bytes, str]: 43 | """ 44 | Get public keys (bytes and base58) from private key (ECDSA) 45 | :param private_key: private key 46 | :return: public key bytes, public key base58 47 | """ 48 | public_key = private_key.public_key() 49 | 50 | public_bytes = public_key.public_bytes(encoding=serialization.Encoding.X962, 51 | format=serialization.PublicFormat.UncompressedPoint) 52 | return public_bytes, base58.b58encode(public_bytes).decode('ascii') 53 | 54 | @staticmethod 55 | def get_public_ECDSA_from_base58(public_base58: str) -> ec.EllipticCurvePublicKey: 56 | """ 57 | Get public key ECDSA from public key base58 58 | :param public_base58: public key base58 59 | :return: public key ECDSA 60 | 61 | :raises: 62 | IdentityValidationError: if invalid public base58 key 63 | """ 64 | try: 65 | public_bytes = base58.b58decode(public_base58) 66 | public_ecdsa = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), public_bytes) 67 | return public_ecdsa 68 | except ValueError as err: 69 | raise IdentityValidationError(f'Can not convert public key base58 to ECDSA: \'{err}\'') from err 70 | -------------------------------------------------------------------------------- /iotics/lib/identity/crypto/proof.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import base64 4 | from dataclasses import dataclass 5 | 6 | import jwt 7 | from cryptography.hazmat.primitives import hashes 8 | from cryptography.hazmat.primitives.asymmetric import ec 9 | 10 | from iotics.lib.identity.const import TOKEN_ALGORITHM 11 | from iotics.lib.identity.crypto.issuer import Issuer 12 | from iotics.lib.identity.crypto.jwt import JwtTokenHelper 13 | from iotics.lib.identity.crypto.key_pair_secrets import KeyPairSecrets, KeyPairSecretsHelper 14 | from iotics.lib.identity.error import IdentityInvalidRegisterIssuerError, IdentityValidationError 15 | from iotics.lib.identity.register.document_helper import RegisterDocumentHelper 16 | from iotics.lib.identity.register.resolver import ResolverClient 17 | 18 | 19 | @dataclass(frozen=True) 20 | class Proof: 21 | issuer: Issuer 22 | content: bytes 23 | signature: str 24 | 25 | @staticmethod 26 | def from_challenge_token(resolver_client: ResolverClient, challenge_token: str) -> 'Proof': 27 | """ 28 | Build proof from challenge token. 29 | :param resolver_client: resolver client to get the registered documents 30 | :param challenge_token: jwt challenge token 31 | :return: valid proof 32 | 33 | :raises: 34 | IdentityValidationError: if invalid challenge token 35 | """ 36 | decoded_token = JwtTokenHelper.decode_token(challenge_token) 37 | iss = decoded_token.get('iss') 38 | aud = decoded_token.get('aud') 39 | if not iss or not aud: 40 | raise IdentityValidationError('Invalid challenge token, missing \'iss\' or \'aud\'') 41 | 42 | issuer = Issuer.from_string(iss) 43 | doc = resolver_client.get_document(issuer.did) 44 | get_controller_doc = resolver_client.get_document 45 | issuer_key = RegisterDocumentHelper.get_valid_issuer_key_for_control_only(doc, issuer.name, get_controller_doc) 46 | if not issuer_key: 47 | raise IdentityInvalidRegisterIssuerError(f'Invalid issuer {issuer}') 48 | verified_token = JwtTokenHelper.decode_and_verify_token(challenge_token, issuer_key.public_key_base58, aud) 49 | return Proof(issuer_key.issuer, aud.encode('ascii'), verified_token['proof']) 50 | 51 | @staticmethod 52 | def build(key_pair: KeyPairSecrets, issuer: Issuer, content: bytes) -> 'Proof': 53 | """ 54 | Build a proof. 55 | :param key_pair: secrets used to build the proof signature 56 | :param issuer: proof issuer 57 | :param content: proof content 58 | :return: proof 59 | 60 | :raises: 61 | IdentityValidationError: if invalid secrets 62 | IdentityDependencyError: if incompatible library dependency 63 | """ 64 | private_key = KeyPairSecretsHelper.get_private_key(key_pair) 65 | sig = private_key.sign(content, ec.ECDSA(hashes.SHA256())) 66 | proof = base64.b64encode(sig).decode('ascii') 67 | return Proof(issuer=issuer, content=content, signature=proof) 68 | 69 | 70 | def build_new_challenge_token(proof: Proof, private_key: ec.EllipticCurvePrivateKey) -> str: 71 | """ 72 | Build a new challenge token from a proof. 73 | :param proof: proof 74 | :param private_key: private key 75 | :return: jwt challenge token 76 | 77 | :raises: 78 | IdentityValidationError: if can not encode the token 79 | 80 | """ 81 | try: 82 | return jwt.encode({'iss': str(proof.issuer), 'aud': proof.content.decode('ascii'), 'proof': proof.signature}, 83 | private_key, algorithm=TOKEN_ALGORITHM) # type: ignore 84 | except (TypeError, ValueError) as err: 85 | raise IdentityValidationError(f'Can not create challenge token for {proof}: \'{err}\'') from err 86 | -------------------------------------------------------------------------------- /iotics/lib/identity/error.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | 4 | class IdentityBaseException(Exception): 5 | """ 6 | Identity base exception. All the exceptions from the iotic.lib.identity extends this exception. 7 | """ 8 | 9 | 10 | class IdentityDependencyError(IdentityBaseException): 11 | """ 12 | Raised when an unexpected error is raised by a dependency due to a version incompatibility. 13 | """ 14 | 15 | 16 | class IdentityValidationError(IdentityBaseException): 17 | """ 18 | Raised when a user input is invalid. 19 | """ 20 | 21 | 22 | class IdentityInvalidDocumentError(IdentityValidationError): 23 | """ 24 | Raised when a RegisterDocument is invalid. 25 | """ 26 | 27 | 28 | class IdentityInvalidProofError(IdentityInvalidDocumentError): 29 | """ 30 | Raised when a Proof (for RegisterDocument, Delegation, ...) is invalid. 31 | """ 32 | 33 | 34 | class IdentityInvalidDocumentDelegationError(IdentityInvalidDocumentError): 35 | """ 36 | Raised when a RegisterDocument delegation (authentication or control) is invalid. 37 | """ 38 | 39 | 40 | class IdentityRegisterDocumentKeyConflictError(IdentityInvalidDocumentError): 41 | """ 42 | Raised when a RegisteredDocument key name is not unique. 43 | """ 44 | 45 | 46 | class IdentityRegisterDocumentKeyNotFoundError(IdentityInvalidDocumentError): 47 | """ 48 | Raised when a register document key is not found. 49 | """ 50 | 51 | 52 | class IdentityRegisterIssuerNotFoundError(IdentityInvalidDocumentError): 53 | """ 54 | Raised when a RegisteredDocument issuer is not found. 55 | """ 56 | 57 | 58 | class IdentityInvalidRegisterIssuerError(IdentityInvalidDocumentError): 59 | """ 60 | Raised when a RegisteredDocument issuer is invalid. 61 | """ 62 | 63 | 64 | class IdentityResolverError(IdentityBaseException): 65 | """ 66 | Raised when an error occurs while interacting with the resolver. 67 | """ 68 | 69 | 70 | class IdentityResolverConflictError(IdentityResolverError): 71 | """ 72 | Raised when a register document already exist with a different owners 73 | """ 74 | 75 | 76 | class IdentityResolverDocNotFoundError(IdentityResolverError): 77 | """ 78 | Raised when a RegisterDocument is not found against the resolver. 79 | """ 80 | 81 | 82 | class IdentityResolverCommunicationError(IdentityResolverError): 83 | """ 84 | Raised when a communication error occurs while interacting with the resolver. 85 | """ 86 | 87 | 88 | class IdentityResolverTimeoutError(IdentityResolverError): 89 | """ 90 | Raised when a timeout error occurs while interacting with the resolver. 91 | """ 92 | 93 | 94 | class IdentityResolverHttpError(IdentityResolverError): 95 | """ 96 | Raised when a HTTPError occurs while interacting with the REST resolver. 97 | """ 98 | 99 | 100 | class IdentityResolverHttpDocNotFoundError(IdentityResolverHttpError, IdentityResolverDocNotFoundError): 101 | """ 102 | Raised when a RegisterDocument is not found against the REST resolver. 103 | """ 104 | 105 | 106 | class IdentityAuthenticationFailed(IdentityBaseException): 107 | """ 108 | Raised when verify authentication fails. 109 | """ 110 | 111 | 112 | class IdentityNotAllowed(IdentityAuthenticationFailed): 113 | """ 114 | Raised when identity not allowed for authentication or control. 115 | """ 116 | -------------------------------------------------------------------------------- /iotics/lib/identity/register/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /iotics/lib/identity/register/document.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from dataclasses import dataclass 4 | from typing import Dict, Optional 5 | 6 | from iotics.lib.identity.const import DOCUMENT_CONTEXT, DOCUMENT_MAX_COMMENT_LENGTH, DOCUMENT_MAX_LABEL_LENGTH, \ 7 | DOCUMENT_MAX_URL_LENGTH, DOCUMENT_VERSION 8 | from iotics.lib.identity.crypto.issuer import Issuer 9 | from iotics.lib.identity.crypto.key_pair_secrets import DIDType 10 | from iotics.lib.identity.error import IdentityValidationError 11 | from iotics.lib.identity.register.keys import RegisterAuthenticationPublicKey, RegisterDelegationProof, \ 12 | RegisterPublicKey 13 | 14 | 15 | @dataclass(frozen=True) 16 | class Metadata: 17 | label: Optional[str] = None 18 | comment: Optional[str] = None 19 | url: Optional[str] = None 20 | 21 | def to_dict(self): 22 | ret = {} 23 | if self.label: 24 | ret['label'] = self.label 25 | if self.comment: 26 | ret['comment'] = self.comment 27 | if self.url: 28 | ret['url'] = self.url 29 | return ret 30 | 31 | @staticmethod 32 | def from_dict(data: dict): 33 | """ 34 | Build register metadata from dict. 35 | :param data: register metadata as dict 36 | :return: valid register metadata 37 | 38 | :raises: 39 | IdentityValidationError: if invalid metadata as dict 40 | """ 41 | return Metadata.build(data.get('label'), data.get('comment'), data.get('url')) 42 | 43 | @staticmethod 44 | def build(label: Optional[str], comment: Optional[str], url: Optional[str]): 45 | """ 46 | Build register metadata. 47 | :param label: metadata label 48 | :param comment: metadata comment 49 | :param url: metadata url 50 | :return: valid register metadata 51 | 52 | :raises: 53 | IdentityValidationError: if invalid label 54 | IdentityValidationError: if invalid comment 55 | IdentityValidationError: if invalid url 56 | """ 57 | if label and len(label) > DOCUMENT_MAX_LABEL_LENGTH: 58 | raise IdentityValidationError(f'Document metadata label it too long, max size: \'{label}\'') 59 | if comment and len(comment) > DOCUMENT_MAX_COMMENT_LENGTH: 60 | raise IdentityValidationError(f'Document metadata comment it too long, max size: \'{comment}\'') 61 | if url and len(url) > DOCUMENT_MAX_URL_LENGTH: 62 | raise IdentityValidationError(f'Document metadata url it too long, max size: \'{url}\'') 63 | return Metadata(label, comment, url) 64 | 65 | 66 | @dataclass(frozen=True) 67 | class RegisterDocument: 68 | public_keys: Dict[str, RegisterPublicKey] 69 | auth_keys: Dict[str, RegisterAuthenticationPublicKey] 70 | auth_delegation_proof: Dict[str, RegisterDelegationProof] 71 | control_delegation_proof: Dict[str, RegisterDelegationProof] 72 | did: str 73 | purpose: DIDType 74 | proof: str 75 | revoked: bool 76 | spec_version: str = DOCUMENT_VERSION 77 | metadata: Metadata = Metadata() 78 | creator: Optional[str] = None 79 | update_time: Optional[int] = None 80 | controller: Optional[Issuer] = None 81 | 82 | def to_dict(self) -> dict: 83 | """ 84 | Serialise thee register document to dict. 85 | :return: register document as dict 86 | """ 87 | ret = { 88 | '@context': DOCUMENT_CONTEXT, 89 | 'id': self.did, 90 | 'ioticsSpecVersion': self.spec_version, 91 | 'ioticsDIDType': self.purpose.value, 92 | 'updateTime': self.update_time, 93 | 'proof': self.proof, 94 | 'publicKey': [k.to_dict() for _, k in self.public_keys.items()], 95 | 'authentication': [k.to_dict() for _, k in self.auth_keys.items()], 96 | 'delegateControl': [k.to_dict() for _, k in self.control_delegation_proof.items()], 97 | 'delegateAuthentication': [k.to_dict() for _, k in self.auth_delegation_proof.items()], 98 | 'metadata': self.metadata.to_dict(), 99 | 'revoked': self.revoked 100 | 101 | } 102 | if self.controller: 103 | ret['controller'] = str(self.controller) 104 | if self.creator: 105 | ret['creator'] = self.creator 106 | return ret 107 | -------------------------------------------------------------------------------- /iotics/lib/identity/register/document_helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import random 4 | import string 5 | from typing import Callable, Optional 6 | 7 | import base58 8 | 9 | from iotics.lib.identity.const import ISSUER_SEPARATOR 10 | from iotics.lib.identity.crypto.identity import make_identifier 11 | from iotics.lib.identity.crypto.issuer import Issuer, IssuerKey 12 | from iotics.lib.identity.register.document import RegisterDocument 13 | from iotics.lib.identity.register.keys import RegisterDelegationProof, RegisterKey, RegisterPublicKey 14 | from iotics.lib.identity.validation.identity import IdentityValidation 15 | 16 | GetControllerDocFunc = Callable[[str], RegisterDocument] 17 | 18 | 19 | class RegisterDocumentHelper: 20 | @staticmethod 21 | def get_owner_register_public_key(doc: RegisterDocument) -> Optional[RegisterPublicKey]: 22 | """ 23 | Get the register document initial owner public key 24 | :param doc: existing register document 25 | :return: RegisterPublicKey if found or None 26 | """ 27 | for key in doc.public_keys.values(): 28 | public_bytes = base58.b58decode(key.base58) 29 | key_id = make_identifier(public_bytes) 30 | if key_id == doc.did: # It is the original key 31 | return key 32 | return None 33 | 34 | @staticmethod 35 | def get_issuer_from_public_key(doc: RegisterDocument, public_base58: str) -> Optional[Issuer]: 36 | """ 37 | Find a register key by issuer and returns True/False if found. 38 | Lookup in the register document public keys (and authentication keys if include_auth is set to True). 39 | :param doc: existing register document 40 | :param public_base58: public key to search for as base58 string 41 | :return: Issuer or None 42 | 43 | :raises: 44 | IdentityValidationError: if invalid name or did 45 | """ 46 | for key in doc.public_keys.values(): 47 | if key.base58 == public_base58: 48 | return Issuer.build(doc.did, key.name) 49 | return None 50 | 51 | @staticmethod 52 | def is_issuer_in_keys(issuer_name: str, doc: RegisterDocument, include_auth: bool) -> bool: 53 | """ 54 | Find an issuer key and returns True/False if found 55 | :param issuer_name: #name to search for 56 | :param doc: existing register document 57 | :param include_auth: include authentication keys 58 | :return: True/False if found 59 | """ 60 | return RegisterDocumentHelper.get_issuer_register_key(issuer_name, doc, include_auth) is not None 61 | 62 | @staticmethod 63 | def get_issuer_register_key(issuer_name: str, doc: RegisterDocument, include_auth: bool) -> Optional[RegisterKey]: 64 | """ 65 | Find a register key by issuer. 66 | Lookup in the register document public keys (and authentication keys if include_auth is set to True). 67 | :param issuer_name: #name to search for 68 | :param doc: existing register document 69 | :param include_auth: include authentication keys 70 | :return: RegisterKey or None 71 | """ 72 | pub_key = doc.public_keys.get(issuer_name) 73 | if pub_key: 74 | return pub_key 75 | if include_auth: 76 | return doc.auth_keys.get(issuer_name) 77 | return None 78 | 79 | @staticmethod 80 | def get_issuer_register_delegation_proof(issuer_name: str, doc: RegisterDocument, 81 | include_auth: bool) -> Optional[RegisterDelegationProof]: 82 | """ 83 | Find a register delegation proof by issuer. 84 | Lookup in the register document control delegation proofs 85 | (and authentication delegation proofs if include_auth is set to True). 86 | :param issuer_name: #name to search for 87 | :param doc: existing register document 88 | :param include_auth: include authentication keys 89 | :return: RegisterDelegationProof or None 90 | """ 91 | control_deleg = doc.control_delegation_proof.get(issuer_name) 92 | if control_deleg: 93 | return control_deleg 94 | if include_auth: 95 | return doc.auth_delegation_proof.get(issuer_name) 96 | return None 97 | 98 | @staticmethod 99 | def get_register_delegation_proof_by_controller(controller: Issuer, doc: RegisterDocument, 100 | include_auth: bool) -> Optional[RegisterDelegationProof]: 101 | """ 102 | Find a register delegation proof by controller issuer. 103 | Lookup in the register document control delegation proofs 104 | (and authentication delegation proofs if include_auth is set to True). 105 | :param controller: Issuing controller 106 | :param doc: existing register document 107 | :param include_auth: include authentication keys 108 | :return: RegisterDelegationProof or None 109 | """ 110 | keys = list(doc.control_delegation_proof.values()) 111 | if include_auth: 112 | keys += list(doc.auth_delegation_proof.values()) 113 | for key in keys: 114 | if key.controller == controller: 115 | return key 116 | return None 117 | 118 | @staticmethod 119 | def get_valid_issuer_key_for(doc: RegisterDocument, issuer_name: str, get_controller_doc: GetControllerDocFunc, 120 | include_auth: bool, ) -> Optional[IssuerKey]: 121 | """ 122 | Get a valid issuer key matching issuer name 123 | :param doc: existing register document 124 | :param issuer_name: name of issuer 125 | :param get_controller_doc: resolver discover function 126 | :param include_auth: include authentication keys 127 | :return: IssuerKey or None 128 | 129 | :raises: 130 | IdentityValidationError: if invalid name or did 131 | """ 132 | key = RegisterDocumentHelper.get_issuer_register_key(issuer_name, doc, include_auth) 133 | if key: 134 | return IssuerKey.build(doc.did, issuer_name, key.base58) 135 | 136 | deleg_key = RegisterDocumentHelper.get_issuer_register_delegation_proof(issuer_name, doc, include_auth) 137 | if deleg_key: 138 | controlled_doc = get_controller_doc(deleg_key.controller.did) 139 | key = RegisterDocumentHelper.get_issuer_register_key(issuer_name, controlled_doc, include_auth) 140 | if key: 141 | return IssuerKey.build(doc.did, issuer_name, key.base58) 142 | return None 143 | 144 | @staticmethod 145 | def get_valid_issuer_key_for_control_only(doc: RegisterDocument, issuer_name: str, 146 | get_controller_doc: GetControllerDocFunc) -> Optional[IssuerKey]: 147 | """ 148 | Get a valid issuer key matching issuer name from the control keys and delegation proofs only 149 | :param doc: existing register document 150 | :param issuer_name: name of issuer 151 | :param get_controller_doc: resolver discover function 152 | :return: IssuerKey or None 153 | 154 | :raises: 155 | IdentityValidationError: if invalid name or did 156 | """ 157 | return RegisterDocumentHelper.get_valid_issuer_key_for(doc, issuer_name, get_controller_doc, include_auth=False) 158 | 159 | @staticmethod 160 | def get_valid_issuer_key_for_auth(doc: RegisterDocument, issuer_name: str, 161 | get_controller_doc: GetControllerDocFunc) -> Optional[IssuerKey]: 162 | """ 163 | Get a valid issuer key matching issuer name from the control and authentication keys and delegation proofs 164 | :param doc: existing register document 165 | :param issuer_name: name of issuer 166 | :param get_controller_doc: resolver discover function 167 | :return: IssuerKey or None 168 | 169 | :raises: 170 | IdentityValidationError: if invalid name or did 171 | """ 172 | return RegisterDocumentHelper.get_valid_issuer_key_for(doc, issuer_name, get_controller_doc, include_auth=True) 173 | 174 | @staticmethod 175 | def new_random_name_for_document(doc: RegisterDocument, length: int = 10) -> str: 176 | """ 177 | New random #name that can be added to this document 178 | :param doc: existing register document 179 | :param length: length of name including # 180 | :return: a new random #name (not already existing in this document) 181 | 182 | ;raises: 183 | IdentityValidationError: if invalid key name (eg if length too short/long) 184 | """ 185 | length -= 1 186 | chars = string.ascii_lowercase + string.digits 187 | while True: 188 | new_name = ISSUER_SEPARATOR + ''.join(random.choice(chars) for _ in range(length)) 189 | IdentityValidation.validate_key_name(new_name) 190 | 191 | if not RegisterDocumentHelper.get_issuer_register_key(new_name, doc, True) and \ 192 | not RegisterDocumentHelper.get_issuer_register_delegation_proof(new_name, doc, True): 193 | return new_name 194 | -------------------------------------------------------------------------------- /iotics/lib/identity/register/key_pair.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from dataclasses import dataclass 4 | 5 | from iotics.lib.identity.crypto.issuer import Issuer 6 | from iotics.lib.identity.crypto.key_pair_secrets import KeyPairSecrets 7 | 8 | 9 | @dataclass(frozen=True) 10 | class RegisteredIdentity: 11 | key_pair_secrets: KeyPairSecrets 12 | issuer: Issuer 13 | 14 | @property 15 | def did(self): 16 | return self.issuer.did 17 | 18 | @property 19 | def name(self): 20 | return self.issuer.name 21 | -------------------------------------------------------------------------------- /iotics/lib/identity/register/keys.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | from enum import Enum 6 | from typing import Optional 7 | 8 | from iotics.lib.identity.const import DOCUMENT_AUTHENTICATION_TYPE, DOCUMENT_PUBLIC_KEY_TYPE 9 | from iotics.lib.identity.crypto.issuer import Issuer 10 | from iotics.lib.identity.error import IdentityValidationError 11 | from iotics.lib.identity.validation.identity import IdentityValidation 12 | 13 | 14 | @dataclass(frozen=True) # type: ignore 15 | class RegisterKeyBase(ABC): 16 | name: str 17 | 18 | @abstractmethod 19 | def get_new_key(self, revoked: bool) -> 'RegisterKeyBase': 20 | raise NotImplementedError 21 | 22 | @abstractmethod 23 | def is_equal(self, other: 'RegisterKeyBase') -> bool: 24 | raise NotImplementedError 25 | 26 | 27 | @dataclass(frozen=True) # type: ignore 28 | class RegisterKey(RegisterKeyBase, ABC): 29 | base58: str 30 | revoked: bool 31 | 32 | @abstractmethod 33 | def get_new_key(self, revoked: bool) -> 'RegisterKeyBase': 34 | raise NotImplementedError 35 | 36 | def is_equal(self, other: 'RegisterKey') -> bool: # type: ignore 37 | """ 38 | Check if register key is equal by name, public key base58 and revoked 39 | This is not overriding the default __eq__ because we still need to fully compare the objects 40 | 41 | :param other: Other register key to compare 42 | :return: True if equal 43 | """ 44 | return self.name == other.name and self.base58 == other.base58 and self.revoked == other.revoked 45 | 46 | 47 | @dataclass(frozen=True) 48 | class RegisterPublicKey(RegisterKey): 49 | 50 | def to_dict(self) -> dict: 51 | ret = { 52 | 'id': self.name, 53 | 'type': DOCUMENT_PUBLIC_KEY_TYPE, 54 | 'publicKeyBase58': self.base58, 55 | 'revoked': self.revoked, 56 | } 57 | return ret 58 | 59 | def get_new_key(self, revoked: bool) -> 'RegisterPublicKey': 60 | """ 61 | Get a new register public key from the current setting revoke field. 62 | :param revoked: is revoked 63 | :return: register public key 64 | 65 | :raises: 66 | IdentityValidationError: if invalid register public key 67 | """ 68 | return RegisterPublicKey.build(self.name, self.base58, revoked) 69 | 70 | @staticmethod 71 | def from_dict(data: dict): 72 | """ 73 | Build a register public key from dict. 74 | :param data: register public key as dict 75 | :return: valid register public key 76 | 77 | :raises: 78 | IdentityValidationError: if invalid register public key as dict 79 | """ 80 | try: 81 | return RegisterPublicKey.build(data['id'], data['publicKeyBase58'], 82 | data.get('revoked', False)) 83 | 84 | except (TypeError, KeyError, ValueError) as err: 85 | raise IdentityValidationError(f'Can not parse invalid register public key: \'{err}\'') from err 86 | 87 | @staticmethod 88 | def build(name: str, public_base58: str, revoked: Optional[bool] = False) -> 'RegisterPublicKey': 89 | """ 90 | Build a register public key. 91 | :param name: key name 92 | :param public_base58: public key base58 93 | :param revoked: is revoked key (default=False) 94 | :return: valid register public key 95 | 96 | :raises: 97 | IdentityValidationError: if invalid key name 98 | """ 99 | IdentityValidation.validate_key_name(name) 100 | return RegisterPublicKey(name=name, base58=public_base58, revoked=revoked) # type: ignore 101 | 102 | 103 | @dataclass(frozen=True) 104 | class RegisterAuthenticationPublicKey(RegisterKey): 105 | 106 | def to_dict(self) -> dict: 107 | ret = { 108 | 'id': self.name, 109 | 'type': DOCUMENT_AUTHENTICATION_TYPE, 110 | 'publicKeyBase58': self.base58, 111 | 'revoked': self.revoked, 112 | } 113 | return ret 114 | 115 | def get_new_key(self, revoked: bool) -> 'RegisterAuthenticationPublicKey': 116 | """ 117 | Get a new register authentication public key from the current setting revoke field. 118 | :param revoked: is revoked 119 | :return: register authentication public key 120 | 121 | :raises: 122 | IdentityValidationError: if invalid register authentication public key 123 | """ 124 | return RegisterAuthenticationPublicKey.build(self.name, self.base58, revoked) 125 | 126 | @staticmethod 127 | def from_dict(data: dict): 128 | """ 129 | Build a register authentication public key from dict. 130 | :param data: register authentication public key as dict 131 | :return: valid register authentication public key 132 | 133 | :raises: 134 | IdentityValidationError: if invalid register authentication public key as dict 135 | """ 136 | try: 137 | return RegisterAuthenticationPublicKey.build(data['id'], data['publicKeyBase58'], 138 | data.get('revoked', False)) 139 | except (TypeError, KeyError, ValueError) as err: 140 | raise IdentityValidationError( 141 | f'Can not parse invalid register authentication public key: \'{err}\'') from err 142 | 143 | @staticmethod 144 | def build(name: str, public_base58: str, revoked: Optional[bool] = False) -> 'RegisterAuthenticationPublicKey': 145 | """ 146 | Build a register authentication public key. 147 | :param name: key name 148 | :param public_base58: authentication public key base58 149 | :param revoked: is revoked key (default=False) 150 | :return: valid register authentication public key 151 | 152 | :raises: 153 | IdentityValidationError: if invalid key name 154 | """ 155 | IdentityValidation.validate_key_name(name) 156 | return RegisterAuthenticationPublicKey(name=name, base58=public_base58, revoked=revoked) # type: ignore 157 | 158 | 159 | class DelegationProofType(Enum): 160 | """ 161 | Delegation proof type. 162 | - DID: that means the proof can be used to setup a delegation from single delegating subject. 163 | The signed proof content is the encoded DID Identifier of the delegating subject. 164 | - GENERIC: that means the proof can be used to setup a delegation from several delegating subjects. 165 | The signed proof content is an empty byte array. 166 | """ 167 | DID = 'did' 168 | GENERIC = 'generic' 169 | 170 | @staticmethod 171 | def from_value(value: Optional[str]): 172 | return DelegationProofType(value) if value else None 173 | 174 | 175 | @dataclass(frozen=True) 176 | class RegisterDelegationProof(RegisterKeyBase): 177 | controller: Issuer 178 | proof: str 179 | revoked: bool 180 | proof_type: DelegationProofType = DelegationProofType.DID 181 | 182 | def is_equal(self, other: 'RegisterDelegationProof') -> bool: # type: ignore 183 | """ 184 | Check if register delegation proof is equal by name, controller and revoked 185 | Cannot check proof as this changes every time 186 | This is not overriding the default __eq__ because we still need to fully compare the objects 187 | 188 | :param other: Other register delegation proof to compare 189 | :return: True if equal 190 | """ 191 | return (self.name == other.name 192 | and self.controller == other.controller 193 | and self.revoked == other.revoked 194 | and self.proof_type == other.proof_type) 195 | 196 | def to_dict(self): 197 | return {'id': self.name, 198 | 'controller': str(self.controller), 199 | 'proof': self.proof, 200 | 'proofType': self.proof_type.value, 201 | 'revoked': self.revoked} 202 | 203 | def get_new_key(self, revoked: bool) -> 'RegisterDelegationProof': 204 | """ 205 | Get a new register delegation proof from the current setting revoke field. 206 | :param revoked: is revoked 207 | :return: register delegation proof 208 | 209 | :raises: 210 | IdentityValidationError: if invalid register delegation proof 211 | """ 212 | return RegisterDelegationProof.build(self.name, self.controller, self.proof, revoked) 213 | 214 | @staticmethod 215 | def from_dict(data: dict): 216 | """ 217 | Build a register delegation public key from dict. 218 | :param data: register delegation public key as dict 219 | :return: valid register delegation key 220 | 221 | :raises: 222 | IdentityValidationError: if invalid register delegation public key as dict 223 | """ 224 | try: 225 | controller = Issuer.from_string(data['controller']) 226 | proof_type = DelegationProofType(data.get('proofType', DelegationProofType.DID.value)) 227 | return RegisterDelegationProof.build(data['id'], controller, data['proof'], 228 | data.get('revoked', False), 229 | proof_type) 230 | except (TypeError, KeyError, ValueError) as err: 231 | raise IdentityValidationError(f'Can not parse invalid register delegation proof: \'{err}\'') from err 232 | 233 | @staticmethod 234 | def build(name: str, controller: Issuer, proof: str, revoked: Optional[bool] = False, 235 | p_type: Optional[DelegationProofType] = DelegationProofType.DID) -> 'RegisterDelegationProof': 236 | """ 237 | Build a register delegation public key. 238 | :param name: key name 239 | :param controller: delegation controller 240 | :param proof: delegation proof 241 | :param revoked: is revoked key (default=False) 242 | :param p_type: the type of the proof in use 243 | :return: valid register delegation public key 244 | 245 | :raises: 246 | IdentityValidationError: if invalid key name 247 | IdentityValidationError: if invalid delegation controller 248 | """ 249 | IdentityValidation.validate_key_name(name) 250 | return RegisterDelegationProof(name, controller, proof, revoked, p_type) # type: ignore 251 | -------------------------------------------------------------------------------- /iotics/lib/identity/register/resolver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | from cryptography.hazmat.primitives.asymmetric import ec 6 | 7 | from iotics.lib.identity.crypto.issuer import Issuer 8 | from iotics.lib.identity.register.document import RegisterDocument 9 | 10 | 11 | class ResolverClient(ABC): 12 | 13 | @abstractmethod 14 | def get_document(self, doc_id: str) -> RegisterDocument: 15 | """ 16 | Get a valid register document from the resolver. 17 | :param doc_id: register document decentralised identifier 18 | :return: valid register document 19 | 20 | :raises: 21 | IdentityResolverError: if invalid resolver response 22 | IdentityResolverDocNotFoundError: if document not found 23 | IdentityResolverTimeoutError: if timeout error 24 | IdentityResolverCommunicationError: if communication error 25 | """ 26 | raise NotImplementedError 27 | 28 | @abstractmethod 29 | def register_document(self, document: RegisterDocument, private_key: ec.EllipticCurvePrivateKey, 30 | issuer: Issuer, audience: str = ''): 31 | """ 32 | Register a register document against the Resolver. 33 | :param document: register document 34 | :param issuer: issuer 35 | :param private_key: issuer private key 36 | :param audience: audience 37 | 38 | :raises: 39 | IdentityResolverError: if resolver error 40 | IdentityResolverError: if can not serialize document 41 | IdentityResolverTimeoutError: if timeout error 42 | IdentityResolverCommunicationError: if communication error 43 | """ 44 | raise NotImplementedError 45 | -------------------------------------------------------------------------------- /iotics/lib/identity/register/rest_resolver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from http import HTTPStatus 4 | from typing import Optional, Union 5 | 6 | import requests 7 | from cryptography.hazmat.primitives.asymmetric import ec 8 | 9 | from iotics.lib.identity.crypto.issuer import Issuer 10 | from iotics.lib.identity.crypto.jwt import JwtTokenHelper 11 | from iotics.lib.identity.error import IdentityInvalidRegisterIssuerError, IdentityResolverCommunicationError, \ 12 | IdentityResolverError, IdentityResolverHttpDocNotFoundError, IdentityResolverHttpError, \ 13 | IdentityResolverTimeoutError, IdentityValidationError 14 | from iotics.lib.identity.register.document import RegisterDocument 15 | from iotics.lib.identity.register.document_builder import RegisterDocumentBuilder 16 | from iotics.lib.identity.register.document_helper import GetControllerDocFunc, RegisterDocumentHelper 17 | from iotics.lib.identity.register.resolver import ResolverClient 18 | 19 | 20 | class ResolverSerializer: 21 | @staticmethod 22 | def get_valid_doc_from_token(token: str, get_controller_doc: GetControllerDocFunc) -> RegisterDocument: 23 | """ 24 | Get a valid RegisterDocument from a resolver token. 25 | :param token: resolver token 26 | :param get_controller_doc: get controller register document function 27 | :return: valid register document 28 | 29 | :raises: 30 | IdentityResolverError: if invalid token 31 | IdentityResolverError: if invalid document 32 | """ 33 | try: 34 | unverified = JwtTokenHelper.decode_token(token) 35 | doc = RegisterDocumentBuilder().build_from_dict(unverified['doc']) 36 | issuer = Issuer.from_string(unverified['iss']) 37 | issuer_key = RegisterDocumentHelper.get_valid_issuer_key_for_control_only(doc, issuer.name, 38 | get_controller_doc) 39 | if not issuer_key: 40 | raise IdentityInvalidRegisterIssuerError(f'Invalid issuer {issuer}') 41 | JwtTokenHelper.decode_and_verify_token(token, issuer_key.public_key_base58, unverified['aud']) 42 | return doc 43 | except (KeyError, ValueError, IdentityValidationError, IdentityInvalidRegisterIssuerError) as exc: 44 | raise IdentityResolverError(f'Can not deserialized invalid resolver token: \'{exc}\'') from exc 45 | 46 | @staticmethod 47 | def serialize_to_token(document: RegisterDocument, private_key: ec.EllipticCurvePrivateKey, 48 | issuer: Issuer, audience: str = '') -> str: 49 | """ 50 | Serialize a register document to resolver token. 51 | :param document: register document 52 | :param private_key: token issuer private key 53 | :param issuer: token issuer 54 | :param audience: token audience 55 | :return: resolver token 56 | 57 | :raises: 58 | IdentityResolverError: if can not encode the token 59 | """ 60 | try: 61 | return JwtTokenHelper.create_doc_token(issuer, audience, document, private_key) 62 | except IdentityValidationError as err: 63 | raise IdentityResolverError(f'Can not serialized to register document resolver token: \'{err}\'') from err 64 | 65 | 66 | class RESTResolverRequester: 67 | 68 | def __init__(self, address: str, timeout: Optional[Union[int, float]] = 60.0): 69 | """ 70 | Rest resolver requester. 71 | :param address: http REST resolver url 72 | :param timeout: optional timeout seconds. Default=60s. If set to 0, requests will have no timeout. 73 | """ 74 | self.address = address 75 | self.timeout = None if timeout and timeout <= 0 else timeout 76 | 77 | def get_token(self, doc_id: str) -> str: 78 | """ 79 | Request the REST resolver to get the token associated to the provided doc identifier. 80 | :param doc_id: register document decentralised identifier 81 | :return: resolver token 82 | 83 | :raises: 84 | IdentityResolverError: if invalid resolver response 85 | IdentityResolverHttpDocNotFoundError: if document not found 86 | IdentityResolverHttpError: if http error 87 | IdentityResolverTimeoutError: if timeout error 88 | IdentityResolverCommunicationError: if communication error 89 | 90 | """ 91 | try: 92 | rsp = requests.get( 93 | f'{self.address}/1.0/discover/{doc_id}', 94 | timeout=self.timeout 95 | ) 96 | rsp.raise_for_status() 97 | return rsp.json()['token'] 98 | except (KeyError, ValueError) as exc: 99 | raise IdentityResolverError(f'Unexpected token format received: \'{exc}\'') from exc 100 | except requests.HTTPError as exc: 101 | if exc.response.status_code == HTTPStatus.NOT_FOUND: 102 | raise IdentityResolverHttpDocNotFoundError(f'Identity token for {doc_id} not found') from exc 103 | raise IdentityResolverHttpError('Identity token could not be retrieved') from exc 104 | except requests.Timeout as exc: 105 | raise IdentityResolverTimeoutError(f'Token retrieval from {self.address} ' 106 | f'with timeout: \'{self.timeout}\' timed out') from exc 107 | except requests.RequestException as exc: 108 | raise IdentityResolverCommunicationError(f'Failed to retrieve token from {self.address}') from exc 109 | 110 | def register_token(self, token: str): 111 | """ 112 | Register a new document token against the REST resolver. 113 | :param token: document token 114 | 115 | :raises: 116 | IdentityResolverHttpError: if http error 117 | IdentityResolverTimeoutError: if timeout error 118 | IdentityResolverCommunicationError: if communication error 119 | """ 120 | try: 121 | rsp = requests.post( 122 | f'{self.address}/1.0/register', 123 | headers={'Content-type': 'text/plain'}, 124 | data=token, 125 | timeout=self.timeout 126 | ) 127 | rsp.raise_for_status() 128 | except requests.HTTPError as exc: 129 | raise IdentityResolverHttpError(f'Can not register Resolver token ot {self.address}: \'{exc}\'') from exc 130 | except requests.Timeout as exc: 131 | raise IdentityResolverTimeoutError(f'Token registration with {self.address} ' 132 | f'with timeout: \'{self.timeout}\' timed out') from exc 133 | except requests.RequestException as exc: 134 | raise IdentityResolverCommunicationError( 135 | f'Can not register Resolver token ot {self.address}: \'{exc}\'') from exc 136 | 137 | 138 | class RESTResolverClient(ResolverClient): 139 | 140 | def __init__(self, requester: RESTResolverRequester, serializer: ResolverSerializer): 141 | self.requester = requester 142 | self.serializer = serializer 143 | 144 | def get_document(self, doc_id: str) -> RegisterDocument: 145 | """ 146 | Get a valid register document from the REST resolver. 147 | :param doc_id: register document decentralised identifier 148 | :return: valid register document 149 | 150 | :raises: 151 | IdentityResolverError: if invalid resolver response 152 | IdentityResolverHttpDocNotFoundError: if document not found 153 | IdentityResolverHttpError: if http error 154 | IdentityResolverTimeoutError: if timeout error 155 | IdentityResolverCommunicationError: if communication error 156 | 157 | """ 158 | token = self.requester.get_token(doc_id) 159 | return self.serializer.get_valid_doc_from_token(token, get_controller_doc=self.get_document) 160 | 161 | def register_document(self, document: RegisterDocument, private_key: ec.EllipticCurvePrivateKey, 162 | issuer: Issuer, audience: str = ''): 163 | """ 164 | Register a register document against the REST Resolver. 165 | :param document: register document 166 | :param issuer: issuer 167 | :param private_key: issuer private key 168 | :param audience: audience 169 | 170 | :raises: 171 | IdentityResolverHttpError: if http error 172 | IdentityResolverTimeoutError: if timeout error 173 | IdentityResolverCommunicationError: if communication error 174 | IdentityResolverError: if can not serialize document 175 | """ 176 | token = self.serializer.serialize_to_token(document, private_key, issuer, audience) 177 | self.requester.register_token(token) 178 | 179 | 180 | def get_rest_resolver_client(address: str, timeout: Optional[Union[int, float]] = 60.0) -> RESTResolverClient: 181 | """ 182 | Get a REST resolver client 183 | :param address: http REST resolver url 184 | :param timeout: optional timeout seconds. Default=60s. If set to 0, requests will have no timeout. 185 | :return: REST resolver client 186 | """ 187 | requester = RESTResolverRequester(address, timeout) 188 | serializer = ResolverSerializer() 189 | return RESTResolverClient(requester, serializer) 190 | -------------------------------------------------------------------------------- /iotics/lib/identity/validation/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /iotics/lib/identity/validation/authentication.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | from iotics.lib.identity.crypto.identity import is_same_identifier 3 | from iotics.lib.identity.crypto.issuer import Issuer 4 | from iotics.lib.identity.crypto.jwt import JwtTokenHelper 5 | from iotics.lib.identity.error import IdentityAuthenticationFailed, IdentityInvalidDocumentDelegationError, \ 6 | IdentityInvalidRegisterIssuerError, IdentityNotAllowed, IdentityResolverError, IdentityValidationError 7 | from iotics.lib.identity.register.document import RegisterDocument 8 | from iotics.lib.identity.register.document_helper import RegisterDocumentHelper 9 | from iotics.lib.identity.register.resolver import ResolverClient 10 | from iotics.lib.identity.validation.proof import DelegationValidation 11 | 12 | 13 | class IdentityAuthValidation: 14 | @staticmethod 15 | def is_allowed_for(issuer: Issuer, issuer_doc: RegisterDocument, subject_doc: RegisterDocument, 16 | include_auth: bool) -> bool: 17 | """ 18 | Check if the issuer is allowed for control (authentication if include_auth = True) on the subject register 19 | document. 20 | Issuer is allowed if both the issuer and subject register document are not revoked 21 | AND ( 22 | ( the issuer is the owner of the subject register document 23 | OR 24 | if a include_auth=True the issuer is in the authentication public keys of the subject register document 25 | ) 26 | OR 27 | the issuer is delegated for control (authentication if include_auth = True) with a valid delegation proof 28 | on the subject registered document 29 | ) 30 | 31 | :param issuer: issuer 32 | :param issuer_doc: issuer register document 33 | :param subject_doc: subject register document 34 | :param include_auth: include authentication keys and delegation proof is set to True 35 | :return: True is allowed else False 36 | """ 37 | if issuer_doc.revoked or subject_doc.revoked: 38 | return False 39 | 40 | if is_same_identifier(issuer.did, subject_doc.did): # it is the same document 41 | issuer_key = RegisterDocumentHelper.get_issuer_register_key(issuer.name, subject_doc, include_auth) 42 | if issuer_key and not issuer_key.revoked: 43 | return True 44 | 45 | delegation_proof = RegisterDocumentHelper.get_register_delegation_proof_by_controller(issuer, subject_doc, 46 | include_auth) 47 | if delegation_proof: 48 | try: 49 | DelegationValidation.validate_delegation_from_doc(subject_doc.did, issuer_doc, delegation_proof) 50 | except IdentityInvalidDocumentDelegationError: 51 | return False 52 | return not delegation_proof.revoked 53 | return False 54 | 55 | @staticmethod 56 | def _check_allowed_on_doc_or_controller(resolver_client: ResolverClient, issuer: Issuer, subject_id: str, 57 | include_auth: bool): 58 | """ 59 | Validate if issuer is allowed for control (authentication if include_auth = True) on the register document 60 | associated to the subject decentralised identifier. 61 | Issuer is allowed if both the issuer and subject register document can be fetched 62 | AND ( 63 | if the issuer is allowed on the subject register document 64 | OR 65 | if the issuer is allowed on the subject controller register document 66 | ) 67 | 68 | :param resolver_client: resolver client interface 69 | :param issuer: issuer under validation 70 | :param subject_id: subject register document decentralised identifier 71 | :param include_auth: include authentication keys and delegation proof is set to True 72 | 73 | :raises: 74 | IdentityNotAllowed: if issuer not allowed 75 | """ 76 | try: 77 | issuer_doc = resolver_client.get_document(issuer.did) 78 | subject_doc = resolver_client.get_document(subject_id) 79 | if IdentityAuthValidation.is_allowed_for(issuer, issuer_doc, subject_doc, include_auth): 80 | return 81 | # Check if allowed for controller 82 | if subject_doc.controller: 83 | controller_doc = resolver_client.get_document(subject_doc.controller.did) 84 | if IdentityAuthValidation.is_allowed_for(issuer, issuer_doc, controller_doc, include_auth): 85 | return 86 | except IdentityResolverError as err: 87 | raise IdentityNotAllowed(f'Cannot validate issuer {issuer} is allowed for {subject_id}: {err}') from err 88 | raise IdentityNotAllowed(f'Issuer {issuer} not allowed for {subject_id}') 89 | 90 | @staticmethod 91 | def validate_allowed_for_control(resolver_client: ResolverClient, issuer: Issuer, subject_id: str): 92 | """ 93 | Validate if issuer is allowed for control on the register document associated to the subject decentralised 94 | identifier. 95 | :param resolver_client: resolver client interface 96 | :param issuer: issuer under validation 97 | :param subject_id: subject register document decentralised identifier 98 | 99 | :raises: 100 | IdentityNotAllowed: if the issuer is not allowed for control 101 | """ 102 | IdentityAuthValidation._check_allowed_on_doc_or_controller(resolver_client, issuer, subject_id, 103 | include_auth=False) 104 | 105 | @staticmethod 106 | def validate_allowed_for_auth(resolver_client: ResolverClient, issuer: Issuer, subject_id: str): 107 | """ 108 | Validate if issuer is allowed for authentication on the register document associated to the subject 109 | decentralised identifier. 110 | :param resolver_client: resolver client interface 111 | :param issuer: issuer under validation 112 | :param subject_id: subject register document decentralised identifier 113 | 114 | :raises: 115 | IdentityNotAllowed: if the issuer is not allowed for authentication 116 | """ 117 | IdentityAuthValidation._check_allowed_on_doc_or_controller(resolver_client, issuer, subject_id, 118 | include_auth=True) 119 | 120 | @staticmethod 121 | def verify_authentication(resolver_client: ResolverClient, token: str) -> dict: 122 | """ 123 | Verify if the authentication token is allowed for authentication. 124 | :param resolver_client: resolver client interface 125 | :param token: jwt authentication token 126 | :return: decoded verified authentication token 127 | 128 | :raises: 129 | IdentityAuthenticationFailed: if not allowed for authentication 130 | """ 131 | try: 132 | unverified_token = JwtTokenHelper.decode_token(token) 133 | for field in ('iss', 'sub', 'aud', 'iat', 'exp'): 134 | if field not in unverified_token: 135 | raise IdentityValidationError(f'Invalid token, missing {field} field') 136 | issuer = Issuer.from_string(unverified_token['iss']) 137 | doc = resolver_client.get_document(issuer.did) 138 | get_controller_doc = resolver_client.get_document 139 | issuer_key = RegisterDocumentHelper.get_valid_issuer_key_for_auth(doc, issuer.name, get_controller_doc) 140 | if not issuer_key: 141 | raise IdentityInvalidRegisterIssuerError(f'Invalid issuer {issuer}') 142 | verified_token = JwtTokenHelper.decode_and_verify_token(token, issuer_key.public_key_base58, 143 | unverified_token['aud']) 144 | 145 | IdentityAuthValidation.validate_allowed_for_auth(resolver_client, issuer_key.issuer, verified_token['sub']) 146 | 147 | return {'iss': verified_token['iss'], 148 | 'sub': verified_token['sub'], 149 | 'aud': verified_token['aud'], 150 | 'iat': verified_token['iat'], 151 | 'exp': verified_token['exp']} 152 | except (IdentityValidationError, IdentityResolverError, 153 | IdentityInvalidRegisterIssuerError, IdentityNotAllowed) as err: 154 | raise IdentityAuthenticationFailed('Not authenticated') from err 155 | -------------------------------------------------------------------------------- /iotics/lib/identity/validation/document.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from iotics.lib.identity.crypto.issuer import Issuer 4 | from iotics.lib.identity.crypto.proof import Proof 5 | from iotics.lib.identity.error import IdentityInvalidDocumentError, IdentityInvalidProofError 6 | from iotics.lib.identity.register.document import RegisterDocument 7 | from iotics.lib.identity.register.document_helper import RegisterDocumentHelper 8 | from iotics.lib.identity.register.resolver import ResolverClient 9 | from iotics.lib.identity.validation.proof import DelegationValidation, ProofValidation 10 | 11 | 12 | class DocumentValidation: 13 | 14 | @staticmethod 15 | def validate_new_document_proof(doc: RegisterDocument): 16 | """ 17 | Validate register document proof. 18 | :param doc: register document. 19 | 20 | :raises: 21 | IdentityInvalidDocumentError: if register document initial owner public key has been removed 22 | IdentityInvalidDocumentError: if register document proof is invalid 23 | """ 24 | try: 25 | key = RegisterDocumentHelper.get_owner_register_public_key(doc) 26 | if not key: 27 | raise IdentityInvalidDocumentError(f'Invalid document \'{doc.did}\', no owner public key') 28 | ProofValidation.validate_proof( 29 | Proof(Issuer.build(doc.did, key.name), doc.did.encode('ascii'), doc.proof), key.base58) 30 | except IdentityInvalidProofError as err: 31 | raise IdentityInvalidDocumentError(f'Invalid document \'{doc.did}\' proof: {err}') from err 32 | 33 | @staticmethod 34 | def validate_document_against_resolver(resolver_client: ResolverClient, doc: RegisterDocument): 35 | """ 36 | Validate a register document against the resolver. 37 | :param resolver_client: resolver client interface 38 | :param doc: register document 39 | 40 | :raises: 41 | IdentityInvalidDocumentDelegationError: if one of the register document delegation proof is invalid 42 | """ 43 | for key in doc.control_delegation_proof.values(): 44 | DelegationValidation.validate_delegation(resolver_client, doc.did, key) 45 | 46 | for key in doc.auth_delegation_proof.values(): 47 | DelegationValidation.validate_delegation(resolver_client, doc.did, key) 48 | -------------------------------------------------------------------------------- /iotics/lib/identity/validation/identity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import re 4 | 5 | from iotics.lib.identity.const import IDENTIFIER_ID_PATTERN, IDENTIFIER_NAME_PATTERN, ISSUER_PATTERN 6 | from iotics.lib.identity.error import IdentityValidationError 7 | 8 | 9 | class IdentityValidation: 10 | @staticmethod 11 | def validate_identifier(did: str): 12 | """ 13 | Validate decentralised identifier. 14 | :param did: decentralised identifier 15 | 16 | :raises: 17 | IdentityValidationError: if invalid identifier 18 | """ 19 | result = re.match(IDENTIFIER_ID_PATTERN, did) 20 | if result is None: 21 | raise IdentityValidationError(f'Identifier does not match pattern {did} - {IDENTIFIER_ID_PATTERN}') 22 | 23 | @staticmethod 24 | def validate_issuer_string(issuer: str): 25 | """ 26 | Validate issuer. 27 | :param issuer: issuer as string 28 | 29 | :raises: 30 | IdentityValidationError: if invalid issuer 31 | """ 32 | result = re.match(ISSUER_PATTERN, issuer) 33 | if result is None: 34 | raise IdentityValidationError(f'Identifier does not match pattern {issuer} - {ISSUER_PATTERN}') 35 | 36 | @staticmethod 37 | def validate_key_name(name: str) -> bool: 38 | """ 39 | Validate key name. 40 | :param name: key name 41 | 42 | :raises: 43 | IdentityValidationError: if invalid key name 44 | """ 45 | m = re.match(IDENTIFIER_NAME_PATTERN, name) 46 | if m is None: 47 | raise IdentityValidationError(f'Name is not valid: {name} - {IDENTIFIER_NAME_PATTERN}') 48 | return True 49 | -------------------------------------------------------------------------------- /iotics/lib/identity/validation/proof.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import base64 4 | 5 | from cryptography.exceptions import InvalidSignature 6 | from cryptography.hazmat.primitives import hashes 7 | from cryptography.hazmat.primitives.asymmetric import ec 8 | 9 | from iotics.lib.identity.crypto.identity import is_same_identifier 10 | from iotics.lib.identity.crypto.keys import KeysHelper 11 | from iotics.lib.identity.crypto.proof import Proof 12 | from iotics.lib.identity.error import IdentityInvalidDocumentDelegationError, IdentityInvalidProofError, \ 13 | IdentityResolverError, \ 14 | IdentityValidationError 15 | from iotics.lib.identity.register.document import RegisterDocument 16 | from iotics.lib.identity.register.keys import DelegationProofType, RegisterDelegationProof, RegisterPublicKey 17 | from iotics.lib.identity.register.resolver import ResolverClient 18 | 19 | 20 | class ProofValidation: 21 | @staticmethod 22 | def validate_proof(proof: Proof, public_base58: str): 23 | """ 24 | Validate proof. 25 | :param proof: proof 26 | :param public_base58: public key base 58 used to create the proof 27 | 28 | :raises: 29 | IdentityInvalidProofError: if invalid proof signature 30 | IdentityInvalidProofError: if invalid proof 31 | """ 32 | public_ecdsa = KeysHelper.get_public_ECDSA_from_base58(public_base58) 33 | try: 34 | signature = base64.b64decode(proof.signature) 35 | public_ecdsa.verify(signature, proof.content, ec.ECDSA(hashes.SHA256())) 36 | except InvalidSignature as err: 37 | raise IdentityInvalidProofError('Invalid proof: invalid signature') from err 38 | except ValueError as err: 39 | raise IdentityInvalidProofError(f'Invalid proof: \'{err}\'') from err 40 | 41 | 42 | class DelegationValidation: 43 | @staticmethod 44 | def _is_valid_issuer_or_reusable_proof(deleg_proof: RegisterDelegationProof, public_key: RegisterPublicKey, 45 | doc_id: str): 46 | controller_issuer = deleg_proof.controller 47 | if deleg_proof.proof_type == DelegationProofType.DID: 48 | proof = Proof(controller_issuer, doc_id.encode('ascii'), deleg_proof.proof) 49 | ProofValidation.validate_proof(proof, public_key.base58) 50 | elif deleg_proof.proof_type == DelegationProofType.GENERIC: 51 | proof = Proof(controller_issuer, b'', deleg_proof.proof) 52 | ProofValidation.validate_proof(proof, public_key.base58) 53 | else: 54 | raise IdentityInvalidProofError(f'Invalid proof: invalid type {deleg_proof.proof_type}') 55 | 56 | @staticmethod 57 | def validate_delegation_from_doc(doc_id: str, controller_doc: RegisterDocument, 58 | deleg_proof: RegisterDelegationProof): 59 | """ 60 | Validate register delegation proof against the deleagtion controller register document. 61 | :param doc_id: decentralised id of the register document owning the register delegation proof 62 | :param controller_doc: delegation controller register document 63 | :param deleg_proof: register delegation proof under validation 64 | 65 | :raises: 66 | IdentityInvalidDocumentDelegationError: if controller issuer does not belongs to the controller document 67 | public keys 68 | IdentityInvalidDocumentDelegationError: if invalid register delegation proof signature 69 | """ 70 | try: 71 | controller_issuer = deleg_proof.controller 72 | public_key = controller_doc.public_keys.get(controller_issuer.name) 73 | if not public_key: 74 | raise IdentityValidationError(f'Public key \'{controller_issuer.name}\' not found' 75 | f' on controller doc \'{controller_doc.did}\'') 76 | DelegationValidation._is_valid_issuer_or_reusable_proof(deleg_proof, public_key, doc_id) 77 | except IdentityValidationError as err: 78 | raise IdentityInvalidDocumentDelegationError(f'Invalid delegation for doc \'{doc_id}\'' 79 | f' with controller: \'{deleg_proof.name}\': {err}') from err 80 | 81 | @staticmethod 82 | def validate_delegation(resolver_client: ResolverClient, doc_id: str, deleg_proof: RegisterDelegationProof): 83 | """ 84 | Validate register delegation proof. 85 | :param resolver_client: resolver client interface 86 | :param doc_id: decentralised id of the register document owning the register delegation proof 87 | :param deleg_proof: register delegation proof under validation 88 | 89 | :raises: 90 | IdentityInvalidDocumentDelegationError: if the register delegation proof is invalid 91 | IdentityInvalidDocumentDelegationError: if the register delegation proof controller can not be fetched 92 | from the resolver 93 | """ 94 | try: 95 | if is_same_identifier(doc_id, str(deleg_proof.controller)): 96 | raise IdentityInvalidDocumentDelegationError(f'Delegation on self no allowed on doc \'{doc_id}\'') 97 | 98 | controller_doc = resolver_client.get_document(deleg_proof.controller.did) 99 | DelegationValidation.validate_delegation_from_doc(doc_id, controller_doc, deleg_proof) 100 | except IdentityResolverError as err: 101 | raise IdentityInvalidDocumentDelegationError(f'Invalid delegation for doc \'{doc_id}\'' 102 | f' with controller: \'{deleg_proof.name}\': {err}') from err 103 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | 4 | [mypy] 5 | strict_optional = false 6 | 7 | [flake8] 8 | exclude = 9 | env 10 | venv 11 | .tox 12 | ignore = 13 | # line break before binary operator (keeping W504: line break after binary operator) 14 | W503 15 | max-line-length = 120 16 | max-complexity = 15 17 | 18 | 19 | [metadata] 20 | name = iotics-identity 21 | version = 2.1.2 22 | description = Iotics DID specification Python library 23 | long_description = file: README.md 24 | long_description_content_type = text/markdown 25 | author = Iotics 26 | author_email = info@iotics.com 27 | license_file = LICENSE 28 | platform = any 29 | 30 | url = https://github.com/Iotic-Labs/iotics-identity-py 31 | keywords = iotics, did, decentralised identity, decentralized identity, digital twin 32 | python_requires = >=3.8 33 | classifiers = 34 | Intended Audience :: Developers 35 | License :: OSI Approved :: Apache Software License 36 | Operating System :: OS Independent 37 | Programming Language :: Python 38 | Programming Language :: Python :: 3.8 39 | Programming Language :: Python :: 3.9 40 | Programming Language :: Python :: 3.10 41 | Programming Language :: Python :: 3.11 42 | Programming Language :: Python :: 3.12 43 | Topic :: Software Development :: Libraries :: Python Modules 44 | project_urls = 45 | Bug Tracker = https://github.com/Iotic-Labs/iotics-identity-py/issues 46 | Changelog = https://github.com/Iotic-Labs/iotics-identity-py/releases 47 | 48 | [options] 49 | zip_safe = True 50 | include_package_data = True 51 | packages = find: 52 | install_requires = 53 | requests>=2.32.0 54 | base58==2.1.1 55 | PyJWT==2.7.0 56 | mnemonic==0.20 57 | cryptography>=43.0.1,<44 58 | 59 | [options.extras_require] 60 | dev = 61 | tox==4.6.4 62 | 63 | test = 64 | pytest==7.4.0 65 | pytest-bdd==6.1.1 66 | pytest-cov==4.1.0 67 | pytest-html==3.2.0 68 | requests-mock==1.11.0 69 | 70 | lint = 71 | flake8==6.0.0 72 | pylint==2.17.4 73 | pylint-quotes==0.2.3 74 | mypy==1.4.1 75 | types-requests 76 | 77 | 78 | [options.packages.find] 79 | exclude = 80 | test* 81 | 82 | [options.entry_points] 83 | console_scripts = 84 | iotics-identity-create-seed=iotics.lib.identity.api.advanced_api:AdvancedIdentityLocalApi.create_seed 85 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from setuptools import setup 4 | 5 | if __name__ == '__main__': 6 | setup() 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/behaviour/common.py: -------------------------------------------------------------------------------- 1 | from iotics.lib.identity.const import ISSUER_SEPARATOR 2 | from typing import Dict, Optional 3 | 4 | import pytest 5 | 6 | from iotics.lib.identity.api.advanced_api import AdvancedIdentityLocalApi 7 | from iotics.lib.identity.crypto.issuer import Issuer 8 | from iotics.lib.identity.crypto.jwt import JwtTokenHelper 9 | from iotics.lib.identity.crypto.key_pair_secrets import build_agent_secrets, build_twin_secrets, build_user_secrets, \ 10 | DIDType, KeyPairSecrets, KeyPairSecretsHelper, SeedMethod 11 | from iotics.lib.identity.error import IdentityNotAllowed, IdentityResolverHttpDocNotFoundError 12 | from iotics.lib.identity.register.document import RegisterDocument 13 | from iotics.lib.identity.register.key_pair import RegisteredIdentity 14 | from iotics.lib.identity.register.resolver import ResolverClient 15 | from iotics.lib.identity.register.rest_resolver import RESTResolverRequester 16 | # Globals 17 | from iotics.lib.identity.validation.authentication import IdentityAuthValidation 18 | 19 | 20 | class RESTRequesterTest(RESTResolverRequester): 21 | def __init__(self, doc_tokens: Dict[str, str] = None): 22 | self.doc_tokens = doc_tokens or {} 23 | 24 | def get_token(self, doc_id: str) -> str: 25 | token = self.doc_tokens.get(doc_id.split(ISSUER_SEPARATOR)[0]) 26 | if not token: 27 | raise IdentityResolverHttpDocNotFoundError(doc_id) 28 | return token 29 | 30 | def register_token(self, token: str): 31 | decoded_token = JwtTokenHelper.decode_token(token) 32 | self.doc_tokens[decoded_token['doc']['id']] = token 33 | 34 | 35 | def get_secrets_by_type(seed: bytes, key_name: str, purpose: DIDType, 36 | seed_method: SeedMethod = SeedMethod.SEED_METHOD_BIP39) -> KeyPairSecrets: 37 | if purpose == DIDType.TWIN: 38 | return build_twin_secrets(seed, key_name, seed_method) 39 | if purpose == DIDType.AGENT: 40 | return build_agent_secrets(seed, key_name, seed_method) 41 | return build_user_secrets(seed, key_name, seed_method) 42 | 43 | 44 | class SetupError(Exception): 45 | def __init__(self, msg): 46 | super().__init__(f'Tests setup error: {msg}') 47 | 48 | 49 | def assert_owner_pub_key_exist(doc: RegisterDocument, owner_name: str, pub_key_base58: str): 50 | owner_key = doc.public_keys.get(owner_name) 51 | assert owner_key, f'Doc {doc.purpose} owner key {owner_name} not found in the register document' 52 | assert not owner_key.revoked, f'Doc {doc.purpose} owner key should not be revoked' 53 | assert owner_key.base58 == pub_key_base58, f'Doc {doc.purpose} invalid owner public key base58' 54 | 55 | 56 | def assert_owner_key(doc: RegisterDocument, owner_name: str, registered_identity: RegisteredIdentity): 57 | key_pair = KeyPairSecretsHelper.get_key_pair(registered_identity.key_pair_secrets) 58 | assert_owner_pub_key_exist(doc, owner_name, key_pair.public_base58) 59 | 60 | 61 | def get_allowed_for_control_error(resolver_client: ResolverClient, issuer: Issuer, 62 | doc_id: str) -> Optional[Exception]: 63 | try: 64 | IdentityAuthValidation.validate_allowed_for_control(resolver_client, issuer, doc_id) 65 | except IdentityNotAllowed as err: 66 | return err 67 | return None 68 | 69 | 70 | def get_allowed_for_auth_error(resolver_client: ResolverClient, issuer: Issuer, 71 | doc_id: str) -> Optional[Exception]: 72 | try: 73 | IdentityAuthValidation.validate_allowed_for_auth(resolver_client, issuer, doc_id) 74 | except IdentityNotAllowed as err: 75 | return err 76 | return None 77 | 78 | 79 | def get_allowed_for_auth_and_control_error(resolver_client: ResolverClient, owner_issuer: Issuer, 80 | doc_id: str) -> Optional[Exception]: 81 | return get_allowed_for_auth_error(resolver_client, owner_issuer, doc_id) or \ 82 | get_allowed_for_control_error(resolver_client, owner_issuer, doc_id) 83 | 84 | 85 | def assert_owner_is_allowed(resolver_client: ResolverClient, owner_issuer: Issuer, doc_id: str): 86 | try: 87 | IdentityAuthValidation.validate_allowed_for_auth(resolver_client, owner_issuer, doc_id) 88 | IdentityAuthValidation.validate_allowed_for_control(resolver_client, owner_issuer, doc_id) 89 | except IdentityNotAllowed as err: 90 | assert False, f'Owner should be allowed for control and authentication on the document: {err}' 91 | 92 | 93 | def assert_owner_not_allowed_anymore(resolver_client: ResolverClient, owner_issuer: Issuer, doc_id: str): 94 | with pytest.raises(IdentityNotAllowed): 95 | IdentityAuthValidation.validate_allowed_for_auth(resolver_client, owner_issuer, doc_id) 96 | with pytest.raises(IdentityNotAllowed): 97 | IdentityAuthValidation.validate_allowed_for_control(resolver_client, owner_issuer, doc_id) 98 | 99 | 100 | def assert_newly_created_registered_identity(seed: bytes, key_name: str, identity_name: str, seed_method: SeedMethod, 101 | registered_identity: RegisteredIdentity, purpose: DIDType): 102 | expected_secrets = get_secrets_by_type(seed, key_name, purpose, seed_method) 103 | assert registered_identity.key_pair_secrets == expected_secrets, f'{purpose}: Invalid registered identity secrets' 104 | key_pair = KeyPairSecretsHelper.get_key_pair(expected_secrets) 105 | assert registered_identity.issuer.did == AdvancedIdentityLocalApi.create_identifier(key_pair.public_bytes), \ 106 | f'{purpose}: Invalid registered identity issuer did' 107 | assert registered_identity.name == identity_name, \ 108 | f'{purpose}: Invalid registered identity issuer name' 109 | -------------------------------------------------------------------------------- /tests/behaviour/features/advanced_identity_api.feature: -------------------------------------------------------------------------------- 1 | Feature: Advanced Identity API 2 | 3 | Scenario: Get a register document from a registered identity 4 | Given a registered identity 5 | When I get the associated document 6 | Then the registered identity issuer did is equal to the document did 7 | 8 | Scenario: Register identity owning the document is in the document public key 9 | Given a registered identity owning the document 10 | When I get the associated document 11 | Then the register document has the registered identity public key 12 | 13 | Scenario: Register identity owning the document is allowed for control and authentication 14 | Given a registered identity owning the document 15 | When I check if the registered identity is allowed for control and authentication on the associated document 16 | Then the registered identity is allowed 17 | 18 | Scenario: Several registered identity can belong to the same document 19 | Given a register document with several owners 20 | When I get the associated document 21 | Then the register document has several public keys 22 | 23 | Scenario: Add a register document owner 24 | Given an register document I owned and a new owner name and public key 25 | When I add the new owner to the document 26 | Then the new owner is allowed for authentication and control on the document 27 | 28 | Scenario: Remove a register document owner 29 | Given an register document I owned and an other existing owner name and public key 30 | When I remove the other owner from the document 31 | Then the removed owner is not allowed for authentication or control on the document 32 | 33 | Scenario: Revoke a register document owner 34 | Given an register document I owned and an other existing owner name and public key 35 | When I revoke the other owner key 36 | Then the revoked owner is not allowed for authentication or control on the document 37 | 38 | Scenario: Add an authentication key to a register document 39 | Given a register document I owned and a new authentication name and public key 40 | When I add the new authentication key to the document 41 | Then the authentication key owner is allowed for authentication on the document 42 | 43 | Scenario: Remove an authentication key from a register document 44 | Given a register document I owned and an existing authentication name and public key 45 | When I remove the authentication key from the document 46 | Then the removed authentication key owner is not allowed for authentication on the document 47 | 48 | Scenario: Revoke an authentication key 49 | Given a register document I owned and an existing authentication name and public key 50 | When I revoke the authentication key 51 | Then the revoked authentication key owner is not allowed for authentication on the document 52 | 53 | # Wording 54 | # DelegatingRId => delegating registered identity => the identity delegating auth or control 55 | # DelegatedRId => delegated registered identity => the identity working on behalf of the delegating identity 56 | Scenario: Add a control delegation between 2 existing registered identities 57 | Given a DelegatingRId owning a document and a DelegatedRId 58 | When the DelegatingRId delegates control to the DelegatedRId 59 | Then the DelegatedRId is allowed for control on the document owned by the DelegatingRId 60 | 61 | Scenario: Add a control delegation did proof (created by an other registered identity) to a document 62 | Given a DelegatingRId owning a document and a delegation did proof created by a DelegatedRId 63 | When I add the control delegation proof to the document owned by the DelegatingRId 64 | Then the DelegatedRId is allowed for control on the document owned by the DelegatingRId 65 | 66 | Scenario: Add a control delegation generic proof (created by an other registered identity) to a document 67 | Given a DelegatingRId owning a document and a delegation generic proof created by a DelegatedRId 68 | When I add the control delegation proof to the document owned by the DelegatingRId 69 | Then the DelegatedRId is allowed for control on the document owned by the DelegatingRId 70 | 71 | Scenario: Remove a control delegation proof from a register document 72 | Given a DelegatingRId owning a document with a control delegation proof created by a DelegatedRId 73 | When I remove the control delegation proof from the document owned by the DelegatingRId 74 | Then the DelegatedRId is not allowed for control on the document owned by the DelegatingRId after delegation remove 75 | 76 | Scenario: Revoke a control delegation proof 77 | Given a DelegatingRId owning a document with a control delegation proof created by a DelegatedRId 78 | When I revoke the control delegation proof 79 | Then the DelegatedRId is not allowed for control on the document owned by the DelegatingRId after delegation revoke 80 | 81 | Scenario: Add an authentication delegation between 2 existing registered identities 82 | Given a DelegatingRId owning a document and a DelegatedRId 83 | When the DelegatingRId delegates authentication to the DelegatedRId 84 | Then the DelegatedRId is allowed for authentication on the document owned by the DelegatingRId 85 | 86 | Scenario: Add an authentication delegation did proof (created by an other registered identity) to a document 87 | Given a DelegatingRId owning a document and a delegation did proof created by a DelegatedRId 88 | When I add the authentication delegation proof to the document owned by the DelegatingRId 89 | Then the DelegatedRId is allowed for authentication on the document owned by the DelegatingRId 90 | 91 | Scenario: Add an authentication delegation generic proof (created by an other registered identity) to a document 92 | Given a DelegatingRId owning a document and a delegation generic proof created by a DelegatedRId 93 | When I add the authentication delegation proof to the document owned by the DelegatingRId 94 | Then the DelegatedRId is allowed for authentication on the document owned by the DelegatingRId 95 | 96 | Scenario: Remove an authentication delegation proof from a register document 97 | Given a DelegatingRId owning a document with an auth delegation proof created by a DelegatedRId 98 | When I remove the authentication delegation proof from the document owned by the DelegatingRId 99 | Then the DelegatedRId is not allowed for authentication on the document owned by the DelegatingRId after delegation remove 100 | 101 | Scenario: Revoke an authentication delegation proof 102 | Given a DelegatingRId owning a document with an auth delegation proof created by a DelegatedRId 103 | When I revoke the authentication delegation proof 104 | Then the DelegatedRId is not allowed for authentication on the document owned by the DelegatingRId after delegation revoke 105 | 106 | Scenario: Authentication delegation is still valid if the delegated identity has several owners and the key used in the proof is revoked 107 | Given a DelegatingRId owning a document with an auth delegation proof created by a DelegatedRId with several owner 108 | When the DelegatedRId owner used for the proof is revoked 109 | Then the DelegatedRId is still allowed for authentication on the document owned by the DelegatingRId 110 | 111 | Scenario: Authentication delegation is not valid if the delegated identity has several owners and the key used in the proof is removed 112 | Given a DelegatingRId owning a document with an auth delegation proof created by a DelegatedRId with several owner 113 | When the DelegatedRId owner used for the proof is removed 114 | Then the DelegatedRId is not allowed for authentication on the document owned by the DelegatingRId anymore 115 | 116 | Scenario: Control delegation is still valid if the delegated identity has several owners and the key used in the proof is revoked 117 | Given a DelegatingRId owning a document with a control delegation proof created by a DelegatedRId with several owner 118 | When the DelegatedRId owner used for the proof is revoked 119 | Then the DelegatedRId is still allowed for control on the document owned by the DelegatingRId 120 | 121 | Scenario: Control delegation is not valid if the delegated identity has several owners and the key used in the proof is removed 122 | Given a DelegatingRId owning a document with a control delegation proof created by a DelegatedRId with several owner 123 | When the DelegatedRId owner used for the proof is removed 124 | Then the DelegatedRId is not allowed for control on the document owned by the DelegatingRId anymore 125 | 126 | Scenario: Document controller is allowed for auth and control 127 | Given a registered identity owning a document and a controller (registered identity) 128 | When I set the controller on my document 129 | Then the controller is allowed for control and authentication 130 | -------------------------------------------------------------------------------- /tests/behaviour/features/high_level_identity_api.feature: -------------------------------------------------------------------------------- 1 | Feature: High Level Identity API 2 | 3 | Scenario: Create user and agent with authentication delegation 4 | Given a user seed, a user key name, an agent seed, and agent key name and a delegation name 5 | When I create user and agent with authentication delegation 6 | Then the user and agent documents are created and registered with authentication delegation 7 | 8 | Scenario: Create a Twin 9 | Given a twin seed and twin a key name 10 | When I create a twin 11 | Then the twin document is created and registered 12 | 13 | Scenario: Create a Twin with control delegation 14 | Given a twin seed, a twin key name and a registered agent identity 15 | When I create a twin with control delegation 16 | Then the twin document is created and registered with control delegation 17 | 18 | Scenario: Create an agent token 19 | Given a registered agent identity with auth delegation for a user_did and a duration 20 | When I create an agent auth token 21 | Then an authorized token is created 22 | 23 | Scenario: Get ownership of a twin 24 | Given a registered twin identity, a registered user identity and a new owner name 25 | When the user takes ownership of the registered twin 26 | Then the twin document is updated with the new owner 27 | -------------------------------------------------------------------------------- /tests/behaviour/features/identity_api.feature: -------------------------------------------------------------------------------- 1 | Feature: Identity API 2 | 3 | Scenario: Create user identity with default seed method 4 | Given a user seed and a user key name 5 | When I create a user 6 | Then the user register document is created, the associated user identity is returned and the user owns the document 7 | 8 | Scenario: Create user identity with legacy seed method 9 | Given a user seed, a user key name and the legacy seed method 10 | When I create a user 11 | Then the user register document is created, the associated user identity is returned and the user owns the document 12 | 13 | Scenario: Create agent identity with default seed method 14 | Given an agent seed and an agent key name 15 | When I create an agent 16 | Then the agent register document is created, the associated agent identity is returned and the agent owns the document 17 | 18 | Scenario: Create agent identity with legacy seed method 19 | Given an agent seed, an agent key name and the legacy seed method 20 | When I create an agent 21 | Then the agent register document is created, the associated agent identity is returned and the agent owns the document 22 | 23 | Scenario: Create twin identity with default seed method 24 | Given a twin seed and a twin key name 25 | When I create a twin 26 | Then the twin register document is created, the associated twin identity is returned and the twin owns the document 27 | 28 | Scenario: Create twin identity with legacy seed method 29 | Given a twin seed, a twin key name and the legacy seed method 30 | When I create a twin 31 | Then the twin register document is created, the associated twin identity is returned and the twin owns the document 32 | 33 | Scenario: Create user identity overriding previously created identity 34 | Given an existing registered user identity 35 | When I create the user overriding the document with a new user name 36 | Then the user document is updated with the new name 37 | 38 | Scenario: Create agent identity overriding previously created identity 39 | Given an existing registered agent identity 40 | When I create the agent overriding the document with a new agent name 41 | Then the agent document is updated with the new name 42 | 43 | Scenario: Create twin identity overriding previously created identity 44 | Given an existing registered twin identity 45 | When I create the twin overriding the document with a new twin name 46 | Then the twin document is updated with the new name 47 | 48 | Scenario: Get existing user identity from secrets 49 | Given a user seed and a user key name from a registered identity 50 | When I get the user identity 51 | Then the identity is valid 52 | 53 | Scenario: Get existing agent identity from secrets 54 | Given an agent seed and an agent key name from a registered identity 55 | When I get the agent identity 56 | Then the identity is valid 57 | 58 | Scenario: Get existing twin identity from secrets 59 | Given a twin seed and a twin key name from a registered identity 60 | When I get the twin identity 61 | Then the identity is valid 62 | 63 | Scenario: User delegates authentication to agent 64 | Given a registered user, a registered agent and a delegation name 65 | When User delegates authentication to agent 66 | Then the user document is updated with the agent authentication delegation 67 | 68 | Scenario: Twin delegates control to agent 69 | Given a registered twin, a registered agent and a delegation name 70 | When Twin delegates control to agent 71 | Then the twin document is updated with the agent authentication delegation 72 | 73 | Scenario: Set document controller 74 | Given a registered identity and a controller issuer 75 | When I set the identity register document controller 76 | Then the document is updated with the new controller 77 | 78 | Scenario: Set document creator 79 | Given a registered identity and a creator 80 | When I set the identity register document creator 81 | Then the document is updated with the new creator 82 | 83 | Scenario: Revoke a document 84 | Given a not revoked registered identity 85 | When I revoke the identity register document 86 | Then the document is revoked 87 | 88 | Scenario: Get a registered document 89 | Given an existing registered identity 90 | When I get the registered document 91 | Then the corresponding document is returned 92 | 93 | Scenario: Verify a valid register document 94 | Given an existing registered document 95 | When I verify the document 96 | Then the document is valid 97 | 98 | Scenario: Verify an corrupted register document 99 | Given a corrupted registered document 100 | When I verify the document 101 | Then a validation error is raised 102 | 103 | Scenario: Create authentication token without authentication delegation 104 | Given a register user document and a register agent document without authentication delegation 105 | When I create an authentication token from the agent without delegation 106 | Then the token is not authorized for authentication 107 | 108 | Scenario: Create authentication token with authentication delegation 109 | Given a register user document and a register agent document with authentication delegation 110 | When I create an authentication token from the agent with delegation 111 | Then the token is authorized for authentication 112 | 113 | Scenario: Add new owner to a register document 114 | Given a new owner key name an registered identity register 115 | When I add a new owner 116 | Then the new owner key has been added to the document 117 | 118 | Scenario: Remove owner from a register document 119 | Given a owner key name an registered identity register 120 | When I remove a owner 121 | Then the key has been removed from the document 122 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/api/test_advanced_api_proof.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from iotics.lib.identity import APIDidDelegationProof, APIGenericDelegationProof, APIProof, get_proof_type, Proof 4 | from iotics.lib.identity.register.keys import DelegationProofType 5 | 6 | 7 | def test_can_build_api_proof(valid_issuer, valid_key_pair_secrets): 8 | content = b"some content" 9 | proof = APIProof.build( 10 | valid_key_pair_secrets, 11 | valid_issuer, 12 | content 13 | ) 14 | assert proof.signature, 'the proof should have a not empty signature' 15 | assert proof.issuer == valid_issuer 16 | assert proof.content == content 17 | assert proof.p_type == DelegationProofType.DID 18 | 19 | 20 | def test_can_build_api_did_delegation_proof(doc_did, valid_issuer, valid_key_pair_secrets): 21 | proof = APIDidDelegationProof.build( 22 | valid_key_pair_secrets, 23 | valid_issuer, 24 | doc_did 25 | ) 26 | assert proof.signature, 'the proof should have a not empty signature' 27 | assert proof.issuer == valid_issuer 28 | assert proof.content == doc_did.encode() 29 | assert proof.p_type == DelegationProofType.DID 30 | 31 | 32 | def test_can_build_api_generic_delegation_proof(valid_issuer, valid_key_pair_secrets): 33 | proof = APIGenericDelegationProof.build( 34 | valid_key_pair_secrets, 35 | valid_issuer 36 | ) 37 | assert proof.signature, 'the proof should have a not empty signature' 38 | assert proof.issuer == valid_issuer 39 | assert proof.content == b'' 40 | assert proof.p_type == DelegationProofType.GENERIC 41 | 42 | 43 | def test_get_proof_type_support_legacy_proof(valid_issuer, valid_key_pair_secrets): 44 | proof = Proof.build(valid_key_pair_secrets, valid_issuer, b'content') 45 | assert get_proof_type(proof) == DelegationProofType.DID 46 | 47 | 48 | def test_can_get_proof_type_from_api_did_delegation_proof(doc_did, valid_issuer, valid_key_pair_secrets): 49 | proof = APIDidDelegationProof.build(valid_key_pair_secrets, valid_issuer, doc_did) 50 | assert get_proof_type(proof) == DelegationProofType.DID 51 | 52 | 53 | def test_can_get_proof_type_from_api_generic_delegation_proof(valid_issuer, valid_key_pair_secrets): 54 | proof = APIGenericDelegationProof.build(valid_key_pair_secrets, valid_issuer) 55 | assert get_proof_type(proof) == DelegationProofType.GENERIC 56 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/api/test_high_level_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.error import IdentityRegisterDocumentKeyConflictError 6 | from iotics.lib.identity import HighLevelIdentityApi, AdvancedIdentityRegisterApi 7 | from tests.unit.iotics.lib.identity.fake import ResolverClientTest 8 | 9 | 10 | def test_create_user_and_agent_with_auth_delegation_duplicate_name_different_controller(valid_bip39_seed): 11 | resolver_client = ResolverClientTest(docs={}) 12 | api = HighLevelIdentityApi(AdvancedIdentityRegisterApi(resolver_client)) 13 | 14 | user_name = '#user' 15 | agent_name = '#agent' 16 | agent_name2 = '#agent2' 17 | deleg_name = '#AuthDeleg' 18 | 19 | api.create_user_and_agent_with_auth_delegation( 20 | user_seed=valid_bip39_seed, user_key_name=user_name, user_name=user_name, 21 | agent_seed=valid_bip39_seed, agent_key_name=agent_name, agent_name=agent_name, 22 | delegation_name=deleg_name 23 | ) 24 | 25 | with pytest.raises(IdentityRegisterDocumentKeyConflictError) as exc: 26 | api.create_user_and_agent_with_auth_delegation( 27 | user_seed=valid_bip39_seed, user_key_name=user_name, user_name=user_name, 28 | agent_seed=valid_bip39_seed, agent_key_name=agent_name2, agent_name=agent_name2, 29 | delegation_name=deleg_name 30 | ) 31 | assert "Authentication delegation name '#AuthDeleg' already in use" in str(exc.value) 32 | 33 | 34 | def test_create_user_and_agent_with_auth_delegation_unspecified_name_different_controller(valid_bip39_seed): 35 | resolver_client = ResolverClientTest(docs={}) 36 | api = HighLevelIdentityApi(AdvancedIdentityRegisterApi(resolver_client)) 37 | 38 | user_name = '#user' 39 | agent_name = '#agent' 40 | agent_name2 = '#agent2' 41 | 42 | user_did, _ = api.create_user_and_agent_with_auth_delegation( 43 | user_seed=valid_bip39_seed, user_key_name=user_name, user_name=user_name, 44 | agent_seed=valid_bip39_seed, agent_key_name=agent_name, agent_name=agent_name 45 | ) 46 | 47 | api.create_user_and_agent_with_auth_delegation( 48 | user_seed=valid_bip39_seed, user_key_name=user_name, user_name=user_name, 49 | agent_seed=valid_bip39_seed, agent_key_name=agent_name2, agent_name=agent_name2 50 | ) 51 | 52 | user_doc = resolver_client.docs.get(user_did.did) 53 | assert len(user_doc.auth_delegation_proof) == 2 54 | 55 | api.create_user_and_agent_with_auth_delegation( 56 | user_seed=valid_bip39_seed, user_key_name=user_name, user_name=user_name, 57 | agent_seed=valid_bip39_seed, agent_key_name=agent_name2, agent_name=agent_name2 58 | ) 59 | 60 | user_doc = resolver_client.docs.get(user_did.did) 61 | assert len(user_doc.auth_delegation_proof) == 2 62 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.crypto.issuer import Issuer, IssuerKey 6 | from iotics.lib.identity.crypto.key_pair_secrets import DIDType, KeyPairSecrets, KeyPairSecretsHelper 7 | from iotics.lib.identity.crypto.proof import Proof 8 | from iotics.lib.identity.register.document_builder import RegisterDocumentBuilder 9 | 10 | 11 | @pytest.fixture 12 | def valid_bip39_seed(): 13 | return b'd2397e8b83cf4a7073a26c1a1cdb6b65' 14 | 15 | 16 | @pytest.fixture 17 | def valid_bip39_mnemonic_english(): 18 | # Note: This matches the d239...6b65 bip39_seed from above 19 | return 'goddess muscle soft human fatal country this hockey great perfect evidence gather industry rack silver ' + \ 20 | 'small cousin another flee silver casino country sugar purse' 21 | 22 | 23 | @pytest.fixture 24 | def valid_bip39_mnemonic_spanish(): 25 | # Note: This matches the d239...6b65 bip39_seed from above 26 | return 'glaciar mojar rueda hueso exponer chupar tanque hijo grano olvido ensayo gaita inmune percha retrato ' + \ 27 | 'rojo cielo alivio fiel retrato brusco chupar sirena peine' 28 | 29 | 30 | @pytest.fixture 31 | def valid_key_pair_secrets(valid_bip39_seed): 32 | return KeyPairSecrets.build(valid_bip39_seed, 'iotics/0/something/user') 33 | 34 | 35 | @pytest.fixture 36 | def valid_private_key(valid_key_pair_secrets): 37 | return KeyPairSecretsHelper.get_private_key(valid_key_pair_secrets) 38 | 39 | 40 | @pytest.fixture 41 | def valid_key_pair(valid_key_pair_secrets): 42 | return KeyPairSecretsHelper.get_key_pair(valid_key_pair_secrets) 43 | 44 | 45 | @pytest.fixture 46 | def valid_issuer_key(valid_issuer, valid_key_pair): 47 | return IssuerKey.build(valid_issuer.did, valid_issuer.name, valid_key_pair.public_base58) 48 | 49 | 50 | @pytest.fixture 51 | def valid_issuer(): 52 | return Issuer.build('did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEgY', '#aName') 53 | 54 | 55 | @pytest.fixture 56 | def other_issuer(): 57 | return Issuer.build('did:iotics:iotHHHmKpPGWyEC4FFo4d6oyzVVk6MXLmEgY', '#aName') 58 | 59 | 60 | @pytest.fixture 61 | def register_doc(valid_key_pair_secrets, valid_issuer): 62 | proof = Proof.build(valid_key_pair_secrets, valid_issuer, content=valid_issuer.did.encode()) 63 | key_pair = KeyPairSecretsHelper.get_key_pair(valid_key_pair_secrets) 64 | return RegisterDocumentBuilder() \ 65 | .add_public_key(valid_issuer.name, key_pair.public_base58, revoked=False) \ 66 | .build(valid_issuer.did, DIDType.USER, proof.signature, revoked=False) 67 | 68 | 69 | @pytest.fixture 70 | def other_key_pair_secrets(): 71 | return KeyPairSecrets.build(b'd2397e8b83cf4a7073a26c1a1cdb6666', 'iotics/0/plop/plop') 72 | 73 | 74 | @pytest.fixture 75 | def other_private_key(other_key_pair_secrets): 76 | return KeyPairSecretsHelper.get_private_key(other_key_pair_secrets) 77 | 78 | 79 | @pytest.fixture 80 | def other_key_pair(other_key_pair_secrets): 81 | return KeyPairSecretsHelper.get_key_pair(other_key_pair_secrets) 82 | 83 | 84 | @pytest.fixture 85 | def doc_did(): 86 | return 'did:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEgY' 87 | 88 | 89 | @pytest.fixture 90 | def deleg_doc_did(): 91 | return 'did:iotics:iotHHHHKpPGWWWC4FFo4d6oyzVVk6MXLmEgY' 92 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.crypto.key_pair_secrets import KEY_PAIR_PATH_PREFIX 6 | 7 | 8 | @pytest.fixture 9 | def valid_seed_16_bytes(): 10 | return b'a' * 16 11 | 12 | 13 | @pytest.fixture 14 | def valid_bip39_seed_32_bytes(): 15 | return b'a' * 32 16 | 17 | 18 | @pytest.fixture 19 | def valid_key_pair_path(): 20 | return f'{KEY_PAIR_PATH_PREFIX}plop' 21 | 22 | 23 | @pytest.fixture 24 | def valid_public_base58(): 25 | return 'Q9F3CfJDDkfdp5s81tReuhaew12Y56askT1RJCdXcbiHcLvBLz2HHmGPxS6XrrkujxLRCHJ6CkkTKfU3izDgMqLa' 26 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/test_identity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.const import IDENTIFIER_PREFIX 6 | from iotics.lib.identity.crypto.identity import is_same_identifier, make_identifier 7 | 8 | 9 | def test_can_make_identifier(valid_key_pair): 10 | new_id = make_identifier(valid_key_pair.public_bytes) 11 | assert new_id.startswith(IDENTIFIER_PREFIX) 12 | 13 | 14 | def test_make_identifier_is_idempotent(valid_key_pair): 15 | assert make_identifier(valid_key_pair.public_bytes) == make_identifier(valid_key_pair.public_bytes) 16 | 17 | 18 | A_DID = 'did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEgY' 19 | 20 | 21 | @pytest.mark.parametrize('id_a,id_b', ((A_DID, A_DID), 22 | (A_DID, f'{A_DID}#Plop'), 23 | (f'{A_DID}#Plop', A_DID), 24 | (f'{A_DID}#AAA', f'{A_DID}#BBB'))) 25 | def test_is_same_identifier_return_true_for_the_same_identifier_ignoring_names(id_a, id_b): 26 | assert is_same_identifier(id_a, id_b) 27 | 28 | 29 | @pytest.mark.parametrize('id_a,id_b', ((A_DID, f'{A_DID[:-1]}K'), 30 | (f'{A_DID}#Plop', f'{A_DID[-1]}K#Plop'))) 31 | def test_is_same_identifier_return_false_for_different_identifier_ignoring_names(id_a, id_b): 32 | assert not is_same_identifier(id_a, id_b) 33 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/test_issuer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.crypto.issuer import Issuer, IssuerKey 6 | from iotics.lib.identity.error import IdentityValidationError 7 | 8 | 9 | @pytest.fixture 10 | def did(): 11 | return 'did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEgY' 12 | 13 | 14 | @pytest.fixture 15 | def name(): 16 | return '#aName' 17 | 18 | 19 | def test_can_build_issuer(did, name): 20 | issuer = Issuer.build(did, name) 21 | assert issuer.did == did 22 | assert issuer.name == name 23 | 24 | 25 | def test_can_build_issuer_from_string(did, name): 26 | issuer_string = f'{did}{name}' 27 | issuer = Issuer.from_string(issuer_string) 28 | assert issuer.did == did 29 | assert issuer.name == name 30 | 31 | 32 | def test_building_issuer_with_invalid_name_raises_validation_error(did): 33 | with pytest.raises(IdentityValidationError): 34 | Issuer.build(did, 'invalidName') 35 | 36 | 37 | def test_building_issuer_from_string_with_invalid_name_raises_validation_error(did): 38 | with pytest.raises(IdentityValidationError): 39 | Issuer.from_string(f'{did}#invalid name') 40 | 41 | 42 | def test_building_issuer_with_invalid_did_raises_validation_error(name): 43 | with pytest.raises(IdentityValidationError): 44 | Issuer.build('invalidDID', name) 45 | 46 | 47 | def test_building_issuer_from_string_with_invalid_did_raises_validation_error(name): 48 | with pytest.raises(IdentityValidationError): 49 | Issuer.from_string(f'invalidDID{name}') 50 | 51 | 52 | def test_can_build_issuer_from_string_with_invalid_string_raises_validation_error(did): 53 | with pytest.raises(IdentityValidationError): 54 | Issuer.from_string(f'{did}aNameWithoutSep') 55 | 56 | 57 | def test_can_build_issuer_key(did, name, valid_public_base58): 58 | issuer_key = IssuerKey.build(did, name, valid_public_base58) 59 | assert issuer_key.issuer == Issuer(did, name) 60 | assert issuer_key.public_key_base58 == valid_public_base58 61 | 62 | 63 | def test_building_issuer_key_with_invalid_issuer_data_raises_validation_error(name, valid_public_base58): 64 | with pytest.raises(IdentityValidationError): 65 | IssuerKey.build('invalid did', name, valid_public_base58) 66 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/test_jwt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from datetime import datetime 4 | 5 | import pytest 6 | 7 | from iotics.lib.identity.crypto.jwt import JwtTokenHelper 8 | from iotics.lib.identity.crypto.keys import KeysHelper 9 | from iotics.lib.identity.error import IdentityValidationError 10 | from iotics.lib.identity.register.document_builder import RegisterDocumentBuilder 11 | 12 | 13 | def compare_doc(register_doc, decoded_doc): 14 | register_doc_as_dict = register_doc.to_dict() 15 | register_doc_as_dict.pop('updateTime') 16 | decoded_doc_as_dict = decoded_doc.to_dict() 17 | decoded_doc_as_dict.pop('updateTime') 18 | assert register_doc_as_dict == decoded_doc_as_dict 19 | 20 | 21 | def test_can_create_doc_token(valid_issuer, valid_private_key, register_doc): 22 | token = JwtTokenHelper.create_doc_token(issuer=valid_issuer, 23 | audience='http://somehting', 24 | doc=register_doc, 25 | private_key=valid_private_key) 26 | assert token 27 | 28 | 29 | def test_create_doc_token_raises_validation_error_if_can_not_create_token(valid_issuer, register_doc): 30 | with pytest.raises(IdentityValidationError): 31 | JwtTokenHelper.create_doc_token(issuer=valid_issuer, 32 | audience='http://somehting', 33 | doc=register_doc, 34 | private_key='no a private key') 35 | 36 | 37 | def test_can_decode_doc_token(valid_issuer, valid_private_key, register_doc): 38 | audience = 'http://somehting' 39 | token = JwtTokenHelper.create_doc_token(issuer=valid_issuer, 40 | audience=audience, 41 | doc=register_doc, 42 | private_key=valid_private_key) 43 | decoded = JwtTokenHelper.decode_token(token) 44 | assert decoded['iss'] == str(valid_issuer) 45 | assert decoded['aud'] == audience 46 | decoded_doc = RegisterDocumentBuilder().build_from_dict(decoded['doc']) 47 | compare_doc(register_doc, decoded_doc) 48 | 49 | 50 | def test_can_decode_and_verify_doc_token(valid_issuer_key, valid_private_key, register_doc): 51 | audience = 'http://somehting' 52 | token = JwtTokenHelper.create_doc_token(issuer=valid_issuer_key.issuer, 53 | audience=audience, 54 | doc=register_doc, 55 | private_key=valid_private_key) 56 | decoded = JwtTokenHelper.decode_and_verify_token(token, valid_issuer_key.public_key_base58, audience) 57 | assert decoded['iss'] == str(valid_issuer_key.issuer) 58 | assert decoded['aud'] == audience 59 | decoded_doc = RegisterDocumentBuilder().build_from_dict(decoded['doc']) 60 | compare_doc(register_doc, decoded_doc) 61 | 62 | 63 | def test_can_create_auth_token(valid_issuer, valid_private_key): 64 | token = JwtTokenHelper.create_auth_token(iss=str(valid_issuer), 65 | sub='did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEEE', 66 | aud='http://somehting', 67 | duration=12, 68 | private_key=valid_private_key) 69 | assert token 70 | 71 | 72 | def test_create_auth_token_raises_validation_error_if_can_not_create_token(valid_issuer): 73 | with pytest.raises(IdentityValidationError): 74 | JwtTokenHelper.create_auth_token(iss=str(valid_issuer), 75 | sub='did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEEE', 76 | aud='http://somehting', 77 | duration=12, 78 | private_key='no a private key') 79 | 80 | 81 | def test_create_auth_token_raises_validation_error_if_negative_duration(valid_issuer, valid_private_key): 82 | with pytest.raises(IdentityValidationError): 83 | JwtTokenHelper.create_auth_token(iss=str(valid_issuer), 84 | sub='did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEEE', 85 | aud='http://somehting', 86 | duration=-12, 87 | private_key=valid_private_key) 88 | 89 | 90 | def test_can_decode_auth_token(valid_issuer, valid_private_key): 91 | audience = 'http://somehting' 92 | subject = 'did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEEE' 93 | start_offset = -20 94 | duration = 12 95 | now = int(datetime.now().timestamp()) 96 | token = JwtTokenHelper.create_auth_token(iss=str(valid_issuer), 97 | sub=subject, 98 | aud='http://somehting', 99 | duration=duration, 100 | private_key=valid_private_key, 101 | start_offset=start_offset) 102 | decoded = JwtTokenHelper.decode_token(token) 103 | assert decoded['iss'] == str(valid_issuer) 104 | assert decoded['aud'] == audience 105 | assert decoded['sub'] == subject 106 | assert decoded['iat'] >= now + start_offset 107 | assert decoded['exp'] >= (now + start_offset) + duration 108 | 109 | 110 | def test_can_decode_and_verify_auth_token(valid_issuer_key, valid_private_key): 111 | audience = 'http://somehting' 112 | subject = 'did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXLmEEE' 113 | start_offset = -20 114 | duration = 12 115 | now = int(datetime.now().timestamp()) 116 | token = JwtTokenHelper.create_auth_token(iss=str(valid_issuer_key.issuer), 117 | sub=subject, 118 | aud='http://somehting', 119 | duration=duration, 120 | private_key=valid_private_key, 121 | start_offset=start_offset) 122 | decoded = JwtTokenHelper.decode_and_verify_token(token, valid_issuer_key.public_key_base58, audience) 123 | assert decoded['iss'] == str(valid_issuer_key.issuer) 124 | assert decoded['aud'] == audience 125 | assert decoded['sub'] == subject 126 | assert decoded['iat'] >= now + start_offset 127 | assert decoded['exp'] >= (now + start_offset) + duration 128 | 129 | 130 | def test_decode_token_raises_validation_error_if_invalid_token(): 131 | with pytest.raises(IdentityValidationError): 132 | JwtTokenHelper.decode_token('not a real token') 133 | 134 | 135 | def test_decode_and_verify_token_raises_validation_error_if_invalid_token(valid_issuer_key): 136 | with pytest.raises(IdentityValidationError): 137 | JwtTokenHelper.decode_and_verify_token('not a real token', valid_issuer_key.public_key_base58, 138 | 'http://audience') 139 | 140 | 141 | def test_decode_and_verify_token_raises_validation_error_if_invalid_issuer_key(valid_issuer_key, register_doc, 142 | valid_private_key, 143 | other_private_key): 144 | audience = 'http://something' 145 | token = JwtTokenHelper.create_doc_token(issuer=valid_issuer_key.issuer, 146 | audience=audience, 147 | doc=register_doc, 148 | private_key=valid_private_key) 149 | 150 | with pytest.raises(IdentityValidationError): 151 | _, public_base58 = KeysHelper.get_public_keys_from_private_ECDSA(other_private_key) 152 | JwtTokenHelper.decode_and_verify_token(token, public_base58, audience) 153 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/test_key_pair_helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | from cryptography.hazmat.primitives.asymmetric import ec 5 | 6 | from iotics.lib.identity.crypto.key_pair_secrets import KeyPairSecrets, KeyPairSecretsHelper 7 | from iotics.lib.identity.error import IdentityValidationError 8 | 9 | 10 | def test_can_convert_seed_to_mnemonic(valid_bip39_seed, valid_bip39_mnemonic_english): 11 | mnemonic = KeyPairSecretsHelper.seed_bip39_to_mnemonic(valid_bip39_seed) 12 | assert mnemonic == valid_bip39_mnemonic_english 13 | 14 | 15 | def test_can_convert_seed_to_mnemonic_with_spanish(valid_bip39_seed, valid_bip39_mnemonic_spanish): 16 | mnemonic = KeyPairSecretsHelper.seed_bip39_to_mnemonic(valid_bip39_seed, lang='spanish') 17 | assert mnemonic == valid_bip39_mnemonic_spanish 18 | 19 | 20 | @pytest.mark.parametrize('invalid_seed,error_val', (('a' * 16, 'Invalid seed format'), 21 | (b'too long' * 945, 'Invalid seed length'), 22 | (b'', 'Invalid seed length'), 23 | (12345, 'Invalid seed format'))) 24 | def test_convert_seed_to_mnemonic_raises_validation_error_if_invalid_seed(invalid_seed, error_val): 25 | with pytest.raises(IdentityValidationError) as err_wrp: 26 | KeyPairSecretsHelper.seed_bip39_to_mnemonic(invalid_seed) 27 | assert error_val in str(err_wrp.value) 28 | 29 | 30 | def test_convert_seed_to_mnemonic_raises_validation_error_if_invalid_language(valid_bip39_seed): 31 | with pytest.raises(IdentityValidationError) as err_wrp: 32 | KeyPairSecretsHelper.seed_bip39_to_mnemonic(valid_bip39_seed, lang='invalid_lang') 33 | assert 'Invalid language for mnemonic:' in str(err_wrp.value) 34 | assert isinstance(err_wrp.value.__cause__, FileNotFoundError) 35 | 36 | 37 | def test_can_convert_mnemonic_to_seed(valid_bip39_mnemonic_english, valid_bip39_seed): 38 | seed = KeyPairSecretsHelper.mnemonic_bip39_to_seed(valid_bip39_mnemonic_english, lang='english') 39 | assert seed == valid_bip39_seed 40 | 41 | 42 | def test_can_convert_mnemonic_to_seed_with_spanish(valid_bip39_mnemonic_spanish, valid_bip39_seed): 43 | seed = KeyPairSecretsHelper.mnemonic_bip39_to_seed(valid_bip39_mnemonic_spanish, lang='spanish') 44 | assert seed == valid_bip39_seed 45 | 46 | 47 | @pytest.mark.parametrize('invalid_mnemonic', ('flee' * 10, 48 | 'flee' * 32)) 49 | def test_convert_mnemonic_to_seed_raises_validation_error_if_invalid_seed(invalid_mnemonic): 50 | with pytest.raises(IdentityValidationError): 51 | KeyPairSecretsHelper.mnemonic_bip39_to_seed(invalid_mnemonic) 52 | 53 | 54 | def test_convert_mnemonic_to_seed_raises_validation_error_if_invalid_language(valid_bip39_mnemonic_english): 55 | with pytest.raises(IdentityValidationError) as err_wrp: 56 | KeyPairSecretsHelper.mnemonic_bip39_to_seed(valid_bip39_mnemonic_english, lang='invalid_lang') 57 | assert isinstance(err_wrp.value.__cause__, FileNotFoundError) 58 | 59 | 60 | def test_validate_bip39_seed_should_not_raise_if_valid_seed(valid_bip39_seed): 61 | KeyPairSecretsHelper.validate_bip39_seed(valid_bip39_seed) 62 | 63 | 64 | def test_validate_bip39_seed_should_raise_if_invalid_seed(): 65 | with pytest.raises(IdentityValidationError): 66 | KeyPairSecretsHelper.validate_bip39_seed(seed=b'invalid') 67 | 68 | 69 | def test_can_get_private_key_from_key_pair_secrets(valid_key_pair_secrets): 70 | private_key = KeyPairSecretsHelper.get_private_key(valid_key_pair_secrets) 71 | assert private_key.key_size == ec.SECP256K1().key_size 72 | assert private_key.curve.name == ec.SECP256K1().name 73 | 74 | 75 | def test_get_private_key_from_key_pair_secrets_raises_validation_error_if_invalid_method(valid_key_pair_secrets): 76 | key_pair_secret = KeyPairSecrets(valid_key_pair_secrets.seed, valid_key_pair_secrets.path, 77 | seed_method='plop', password='') 78 | with pytest.raises(IdentityValidationError): 79 | KeyPairSecretsHelper.get_private_key(key_pair_secret) 80 | 81 | 82 | def test_can_get_public_base58_key(valid_key_pair_secrets, valid_public_base58): 83 | public_base58_key = KeyPairSecretsHelper.get_public_key_base58_from_key_pair_secrets(valid_key_pair_secrets) 84 | assert public_base58_key == valid_public_base58 85 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/test_key_pair_validation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.crypto.key_pair_secrets import build_agent_secrets, build_twin_secrets, build_user_secrets, \ 6 | DIDType, KEY_PAIR_PATH_PREFIX, KeyPairSecrets, SeedMethod 7 | from iotics.lib.identity.error import IdentityValidationError 8 | 9 | 10 | def test_can_create_key_pair_secrets_with_none_seed_method(valid_seed_16_bytes, 11 | valid_key_pair_path): 12 | password = 'a password' 13 | key_pair = KeyPairSecrets.build(seed=valid_seed_16_bytes, 14 | path=valid_key_pair_path, 15 | password=password, 16 | seed_method=SeedMethod.SEED_METHOD_NONE) 17 | 18 | assert key_pair.seed == valid_seed_16_bytes 19 | assert key_pair.path == valid_key_pair_path 20 | assert key_pair.seed_method == SeedMethod.SEED_METHOD_NONE 21 | assert key_pair.password == password 22 | 23 | 24 | def test_can_create_key_pair_secrets_with_default_bip39_seed_method(valid_bip39_seed_32_bytes, 25 | valid_key_pair_path): 26 | password = 'a password' 27 | key_pair = KeyPairSecrets.build(seed=valid_bip39_seed_32_bytes, 28 | path=valid_key_pair_path, 29 | password=password) 30 | 31 | assert key_pair.seed == valid_bip39_seed_32_bytes 32 | assert key_pair.path == valid_key_pair_path 33 | assert key_pair.seed_method == SeedMethod.SEED_METHOD_BIP39 34 | assert key_pair.password == password 35 | 36 | 37 | @pytest.mark.parametrize('invalid_seed,error_val', (('a' * 16, 'Invalid seed format'), 38 | (b'too long' * 945, 'Invalid seed length'), 39 | (b'', 'Invalid seed length'), 40 | (12345, 'Invalid seed format'))) 41 | def test_create_key_pair_raises_validation_error_if_invalid_seed_with_bip39_method(invalid_seed, error_val, 42 | valid_key_pair_path): 43 | with pytest.raises(IdentityValidationError) as err_wpr: 44 | KeyPairSecrets.build(seed=invalid_seed, 45 | path=valid_key_pair_path) 46 | assert error_val in str(err_wpr.value) 47 | 48 | 49 | @pytest.mark.parametrize('invalid_seed,error_val', (('a' * 16, 'Invalid seed format'), 50 | (b'not long enough'[0:3], 'Invalid seed length'), 51 | (b'', 'Invalid seed length'), 52 | (12345, 'Invalid seed format'))) 53 | def test_create_key_pair_raises_validation_error_if_invalid_seed_with_none_method(invalid_seed, error_val, 54 | valid_key_pair_path): 55 | with pytest.raises(IdentityValidationError) as err_wpr: 56 | KeyPairSecrets.build(seed=invalid_seed, 57 | path=valid_key_pair_path, 58 | seed_method=SeedMethod.SEED_METHOD_BIP39) 59 | assert error_val in str(err_wpr.value) 60 | 61 | 62 | def test_create_key_pair_raises_validation_error_if_invalid_path(valid_bip39_seed_32_bytes): 63 | with pytest.raises(IdentityValidationError) as err_wpr: 64 | KeyPairSecrets.build(seed=valid_bip39_seed_32_bytes, 65 | path='invalid path (no starting with Iotics prefix)') 66 | assert 'Invalid key pair path' in str(err_wpr.value) 67 | 68 | 69 | @pytest.mark.parametrize('create_key_pair_with_purpose, expected_path', ( 70 | (build_user_secrets, f'{KEY_PAIR_PATH_PREFIX}/{DIDType.USER}/a_name'), 71 | (build_agent_secrets, f'{KEY_PAIR_PATH_PREFIX}/{DIDType.AGENT}/a_name'), 72 | (build_twin_secrets, f'{KEY_PAIR_PATH_PREFIX}/{DIDType.TWIN}/a_name') 73 | )) 74 | def test_create_key_pair_with_purpose(create_key_pair_with_purpose, expected_path, 75 | valid_bip39_seed_32_bytes): 76 | password = 'a passwd' 77 | name = 'a_name' 78 | key_pair = create_key_pair_with_purpose(seed=valid_bip39_seed_32_bytes, 79 | name=name, 80 | seed_method=SeedMethod.SEED_METHOD_NONE, 81 | password=password) 82 | assert key_pair.seed == valid_bip39_seed_32_bytes 83 | assert key_pair.path == expected_path 84 | assert key_pair.seed_method == SeedMethod.SEED_METHOD_NONE 85 | assert key_pair.password == password 86 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/test_keys.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import hmac 4 | from _sha256 import sha256 5 | from _sha512 import sha512 6 | 7 | import base58 8 | import pytest 9 | from cryptography.hazmat.backends import default_backend 10 | from cryptography.hazmat.primitives.asymmetric import ec 11 | 12 | from iotics.lib.identity.crypto.key_pair_secrets import KeyPairSecretsHelper 13 | from iotics.lib.identity.crypto.keys import KeysHelper 14 | from iotics.lib.identity.error import IdentityValidationError 15 | 16 | 17 | @pytest.fixture 18 | def private_expo(valid_seed_16_bytes): 19 | master = hmac.new(valid_seed_16_bytes, b'passwdPlop', sha512).digest() 20 | return hmac.new(master, b'plopPLOPplop', sha256).hexdigest() 21 | 22 | 23 | @pytest.fixture 24 | def private_ecdsa(): 25 | return ec.derive_private_key(122222222222222222222222222222222, ec.SECP256K1(), default_backend()) 26 | 27 | 28 | @pytest.fixture 29 | def key_pair(valid_key_pair_secrets): 30 | return KeyPairSecretsHelper.get_key_pair(valid_key_pair_secrets) 31 | 32 | 33 | def test_get_private_ecdsa(private_expo): 34 | private_ecdsa = KeysHelper.get_private_ECDSA(private_expo) 35 | assert private_ecdsa 36 | assert private_ecdsa.key_size == ec.SECP256K1().key_size 37 | assert private_ecdsa.curve.name == ec.SECP256K1().name 38 | 39 | 40 | def test_get_public_keys_from_private_ecdsa(private_ecdsa): 41 | public_bytes, public_base58 = KeysHelper.get_public_keys_from_private_ECDSA(private_ecdsa) 42 | assert public_bytes 43 | assert public_base58 44 | assert base58.b58encode(public_bytes).decode('ascii') == public_base58 45 | 46 | public_bytes_bis, public_base58_bis = KeysHelper.get_public_keys_from_private_ECDSA(private_ecdsa) 47 | assert public_bytes == public_bytes_bis 48 | assert public_base58 == public_base58_bis 49 | 50 | 51 | def test_get_public_ecdsa_from_base58(valid_public_base58): 52 | public_bytes = KeysHelper.get_public_ECDSA_from_base58(valid_public_base58) 53 | assert public_bytes.key_size == ec.SECP256K1().key_size 54 | assert public_bytes.curve.name == ec.SECP256K1().name 55 | 56 | 57 | def test_get_public_ecdsa_from_base58_raises_validation_error_if_invalid_key(valid_public_base58): 58 | invalid_public_base58_key = valid_public_base58 + 'a' 59 | with pytest.raises(IdentityValidationError) as err_wrapper: 60 | KeysHelper.get_public_ECDSA_from_base58(invalid_public_base58_key) 61 | assert isinstance(err_wrapper.value.__cause__, ValueError) 62 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/crypto/test_proof.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import jwt 4 | import pytest 5 | from jwt import DecodeError 6 | 7 | from iotics.lib.identity.const import TOKEN_ALGORITHM 8 | from iotics.lib.identity.crypto.issuer import Issuer 9 | from iotics.lib.identity.crypto.jwt import JwtTokenHelper 10 | from iotics.lib.identity.crypto.key_pair_secrets import KeyPairSecrets, SeedMethod 11 | from iotics.lib.identity.crypto.proof import build_new_challenge_token, Proof 12 | from iotics.lib.identity.error import IdentityInvalidRegisterIssuerError, IdentityValidationError 13 | from tests.unit.iotics.lib.identity.fake import ResolverClientTest 14 | 15 | 16 | def test_can_build_proof(valid_key_pair_secrets, valid_issuer): 17 | content = b'a content' 18 | proof = Proof.build(valid_key_pair_secrets, valid_issuer, content) 19 | assert proof.issuer == valid_issuer 20 | assert proof.content == content 21 | assert proof.signature 22 | 23 | 24 | @pytest.fixture 25 | def invalid_key_pair_secrets(): 26 | return KeyPairSecrets(seed=b'invalid secret', path='plop', seed_method=SeedMethod.SEED_METHOD_BIP39, password='') 27 | 28 | 29 | def test_build_proof_raises_validation_error_if_invalid_inputs(invalid_key_pair_secrets, valid_issuer): 30 | with pytest.raises(IdentityValidationError) as err_wrapper: 31 | Proof.build(invalid_key_pair_secrets, valid_issuer, b'a content') 32 | assert isinstance(err_wrapper.value.__cause__, ValueError) 33 | 34 | 35 | def test_can_build_challenge_token(valid_private_key, valid_key_pair_secrets, valid_issuer): 36 | content = b'a content' 37 | proof = Proof.build(valid_key_pair_secrets, valid_issuer, content) 38 | challenge_token = build_new_challenge_token(proof, valid_private_key) 39 | assert challenge_token 40 | decoded = JwtTokenHelper.decode_token(challenge_token) 41 | assert decoded['iss'] == str(valid_issuer) 42 | assert decoded['aud'] == content.decode('ascii') 43 | assert decoded['proof'] == proof.signature 44 | 45 | 46 | def test_build_challenge_token_raises_validation_error_if_can_not_create_token(valid_key_pair_secrets, valid_issuer): 47 | proof = Proof.build(valid_key_pair_secrets, valid_issuer, b'a content') 48 | with pytest.raises(IdentityValidationError) as err_wrapper: 49 | build_new_challenge_token(proof, private_key='not a private key') 50 | assert isinstance(err_wrapper.value.__cause__, ValueError) 51 | 52 | 53 | def test_can_build_proof_from_challenge_token(valid_private_key, valid_key_pair_secrets, valid_issuer, register_doc): 54 | proof = Proof.build(valid_key_pair_secrets, valid_issuer, content=b'a content') 55 | challenge_token = build_new_challenge_token(proof, valid_private_key) 56 | resolver_client = ResolverClientTest(docs={register_doc.did: register_doc}) 57 | 58 | deserialized_proof = Proof.from_challenge_token(resolver_client, challenge_token) 59 | assert deserialized_proof.issuer == proof.issuer 60 | assert deserialized_proof.content == proof.content 61 | assert deserialized_proof.signature == proof.signature 62 | 63 | 64 | def test_build_proof_from_challenge_token_raises_validation_error_if_invalid_token(register_doc): 65 | resolver_client = ResolverClientTest(docs={register_doc.did: register_doc}) 66 | with pytest.raises(IdentityValidationError) as err_wrapper: 67 | Proof.from_challenge_token(resolver_client, 'invalid token') 68 | assert isinstance(err_wrapper.value.__cause__, DecodeError) 69 | 70 | 71 | def test_build_proof_from_challenge_token_raises_validation_error_if_invalid_token_data(register_doc, 72 | valid_private_key): 73 | invalid_challenge_token = jwt.encode({'invalid': 'data'}, valid_private_key, algorithm=TOKEN_ALGORITHM) 74 | 75 | resolver_client = ResolverClientTest(docs={register_doc.did: register_doc}) 76 | with pytest.raises(IdentityValidationError): 77 | Proof.from_challenge_token(resolver_client, invalid_challenge_token) 78 | 79 | 80 | def test_build_proof_from_challenge_token_raises_issuer_error_if_issuer_not_in_doc(register_doc, valid_private_key, 81 | valid_key_pair_secrets): 82 | other_issuer = Issuer.build(register_doc.did, '#OtherIssuer') 83 | proof = Proof.build(valid_key_pair_secrets, other_issuer, content=b'a content') 84 | challenge_token = build_new_challenge_token(proof, valid_private_key) 85 | 86 | resolver_client = ResolverClientTest(docs={register_doc.did: register_doc}) 87 | 88 | with pytest.raises(IdentityInvalidRegisterIssuerError): 89 | Proof.from_challenge_token(resolver_client, challenge_token) 90 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/fake.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from typing import Dict 4 | 5 | from cryptography.hazmat.primitives.asymmetric import ec 6 | 7 | from iotics.lib.identity import RegisterDocumentBuilder 8 | from iotics.lib.identity.const import ISSUER_SEPARATOR 9 | from iotics.lib.identity.crypto.issuer import Issuer 10 | from iotics.lib.identity.error import IdentityResolverDocNotFoundError 11 | from iotics.lib.identity.register.document import RegisterDocument 12 | from iotics.lib.identity.register.resolver import ResolverClient 13 | 14 | 15 | class ResolverClientTest(ResolverClient): 16 | def __init__(self, docs: Dict[str, RegisterDocument] = None): 17 | self.docs = docs or {} 18 | 19 | def get_document(self, doc_id: str) -> RegisterDocument: 20 | doc = self.docs.get(doc_id.split(ISSUER_SEPARATOR)[0]) 21 | if not doc: 22 | raise IdentityResolverDocNotFoundError(doc_id) 23 | return doc 24 | 25 | def register_document(self, document: RegisterDocument, private_key: ec.EllipticCurvePrivateKey, 26 | issuer: Issuer, audience: str = ''): 27 | self.docs[issuer.did] = RegisterDocumentBuilder().build_from_dict(document.to_dict()) 28 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from typing import Iterable 4 | 5 | from iotics.lib.identity.crypto.key_pair_secrets import DIDType 6 | from iotics.lib.identity.register.document import RegisterDocument 7 | from iotics.lib.identity.register.document_builder import RegisterDocumentBuilder 8 | from iotics.lib.identity.register.keys import RegisterPublicKey, RegisterAuthenticationPublicKey, \ 9 | RegisterDelegationProof 10 | 11 | 12 | def get_doc_with_keys(public_keys: Iterable[RegisterPublicKey] = None, 13 | auth_keys: Iterable[RegisterAuthenticationPublicKey] = None, 14 | deleg_control: Iterable[RegisterDelegationProof] = None, 15 | deleg_auth: Iterable[RegisterDelegationProof] = None, 16 | controller: str = None, 17 | did: str = None) -> RegisterDocument: 18 | builder = RegisterDocumentBuilder() 19 | _ = [builder.add_public_key_obj(k) for k in (public_keys or ())] 20 | _ = [builder.add_authentication_key_obj(k) for k in (auth_keys or ())] 21 | _ = [builder.add_control_delegation_obj(k) for k in (deleg_control or ())] 22 | _ = [builder.add_authentication_delegation_obj(k) for k in (deleg_auth or ())] 23 | 24 | return builder.build(did=did or 'did:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEgY', 25 | purpose=DIDType.TWIN, 26 | proof='a proof', 27 | revoked=True, 28 | controller=controller) 29 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/register/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/register/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from uuid import uuid4 4 | 5 | import pytest 6 | 7 | from iotics.lib.identity.const import SUPPORTED_VERSIONS 8 | from iotics.lib.identity.crypto.issuer import Issuer 9 | from iotics.lib.identity.crypto.key_pair_secrets import DIDType, KeyPairSecrets, KeyPairSecretsHelper 10 | from iotics.lib.identity.register.document import Metadata, RegisterDocument 11 | from iotics.lib.identity.register.document_builder import get_unix_time_ms 12 | from iotics.lib.identity.register.keys import RegisterAuthenticationPublicKey, RegisterDelegationProof, \ 13 | RegisterPublicKey 14 | 15 | 16 | def get_public_base_58_key() -> str: 17 | secrets = KeyPairSecrets.build(b'a' * 32, f'iotics/0/something/{uuid4()}') 18 | key_pair = KeyPairSecretsHelper.get_key_pair(secrets) 19 | return key_pair.public_base58 20 | 21 | 22 | @pytest.fixture 23 | def valid_key_name(): 24 | return '#AKeyName' 25 | 26 | 27 | @pytest.fixture 28 | def valid_public_key_base58(): 29 | return get_public_base_58_key() 30 | 31 | 32 | @pytest.fixture 33 | def a_controller(): 34 | return Issuer('did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXEEEEE', '#AController') 35 | 36 | 37 | @pytest.fixture 38 | def b_controller(): 39 | return Issuer('did:iotics:iotHjrmKpPGWyEC4FFo4d6oyzVVk6MXXXXXX', '#AController') 40 | 41 | 42 | @pytest.fixture 43 | def a_proof(): 44 | return 'a_proof_validated_by_the_resolver' 45 | 46 | 47 | @pytest.fixture 48 | def doc_proof(a_proof): 49 | return a_proof 50 | 51 | 52 | @pytest.fixture 53 | def doc_controller(a_controller): 54 | return a_controller 55 | 56 | 57 | @pytest.fixture 58 | def doc_keys(a_controller): 59 | return { 60 | '#pub_key1': RegisterPublicKey(name='#pub_key1', base58=get_public_base_58_key(), revoked=False), 61 | '#pub_key2': RegisterPublicKey(name='#pub_key2', base58=get_public_base_58_key(), revoked=True), 62 | '#auth_key1': RegisterAuthenticationPublicKey(name='#auth_key1', base58=get_public_base_58_key(), 63 | revoked=False), 64 | '#auth_key2': RegisterAuthenticationPublicKey(name='#auth_key2', base58=get_public_base_58_key(), revoked=True), 65 | '#deleg_control_key1': RegisterDelegationProof(name='#deleg_control_key1', controller=a_controller, 66 | proof='a_deleg_proof_validated_by_the_resolver', 67 | revoked=False), 68 | '#deleg_control_key2': RegisterDelegationProof(name='#deleg_control_key2', controller=a_controller, 69 | proof='a_deleg_proof_validated_by_the_resolver', 70 | revoked=True), 71 | '#deleg_auth_key1': RegisterDelegationProof(name='#deleg_auth_key1', controller=a_controller, 72 | proof='a_deleg_proof_validated_by_the_resolver', revoked=False), 73 | '#deleg_auth_key2': RegisterDelegationProof(name='#deleg_auth_key2', controller=a_controller, 74 | proof='a_deleg_proof_validated_by_the_resolver', revoked=True), 75 | 76 | } 77 | 78 | 79 | @pytest.fixture 80 | def min_doc_owner_pub_key(): 81 | return RegisterPublicKey('#Owner', 'pubbase58 value', revoked=False) 82 | 83 | 84 | @pytest.fixture 85 | def minimal_doc(doc_did, doc_proof, min_doc_owner_pub_key): 86 | return RegisterDocument(did=doc_did, 87 | purpose=DIDType.TWIN, 88 | proof=doc_proof, 89 | revoked=True, 90 | public_keys={min_doc_owner_pub_key.name: min_doc_owner_pub_key}, 91 | auth_keys={}, 92 | control_delegation_proof={}, 93 | auth_delegation_proof={}, 94 | update_time=get_unix_time_ms()) 95 | 96 | 97 | @pytest.fixture 98 | def full_doc(doc_keys, doc_did, doc_proof, doc_controller): 99 | return RegisterDocument(did=doc_did, 100 | purpose=DIDType.TWIN, 101 | proof=doc_proof, 102 | revoked=True, 103 | metadata=Metadata.build('a label', 'a comment', 'http://a/url'), 104 | creator='did:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MEEEEgY', 105 | spec_version=SUPPORTED_VERSIONS[0], 106 | update_time=get_unix_time_ms(), 107 | controller=doc_controller, 108 | public_keys={doc_keys['#pub_key1'].name: doc_keys['#pub_key1'], 109 | doc_keys['#pub_key2'].name: doc_keys['#pub_key2']}, 110 | auth_keys={doc_keys['#auth_key1'].name: doc_keys['#auth_key1'], 111 | doc_keys['#auth_key2'].name: doc_keys['#auth_key2']}, 112 | control_delegation_proof={ 113 | doc_keys['#deleg_control_key1'].name: doc_keys['#deleg_control_key1'], 114 | doc_keys['#deleg_control_key2'].name: doc_keys['#deleg_control_key2']}, 115 | auth_delegation_proof={doc_keys['#deleg_auth_key1'].name: doc_keys['#deleg_auth_key1'], 116 | doc_keys['#deleg_auth_key2'].name: doc_keys['#deleg_auth_key2']} 117 | ) 118 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/register/test_document.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.const import DOCUMENT_MAX_COMMENT_LENGTH, DOCUMENT_MAX_LABEL_LENGTH, DOCUMENT_MAX_URL_LENGTH 6 | from iotics.lib.identity.error import IdentityValidationError 7 | from iotics.lib.identity.register.document import Metadata 8 | 9 | 10 | def test_can_serialise_full_document_to_dict(full_doc, doc_keys): 11 | dict_doc = full_doc.to_dict() 12 | assert dict_doc.pop('updateTime') == full_doc.update_time 13 | 14 | assert dict_doc == {'@context': 'https://w3id.org/did/v1', 15 | 'id': full_doc.did, 16 | 'ioticsDIDType': full_doc.purpose.value, 17 | 'ioticsSpecVersion': full_doc.spec_version, 18 | 'controller': str(full_doc.controller), 19 | 'creator': full_doc.creator, 20 | 'metadata': full_doc.metadata.to_dict(), 21 | 'revoked': full_doc.revoked, 22 | 'proof': full_doc.proof, 23 | 'publicKey': [doc_keys['#pub_key1'].to_dict(), doc_keys['#pub_key2'].to_dict()], 24 | 'authentication': [doc_keys['#auth_key1'].to_dict(), doc_keys['#auth_key2'].to_dict()], 25 | 'delegateControl': [doc_keys['#deleg_control_key1'].to_dict(), 26 | doc_keys['#deleg_control_key2'].to_dict()], 27 | 'delegateAuthentication': [doc_keys['#deleg_auth_key1'].to_dict(), 28 | doc_keys['#deleg_auth_key2'].to_dict()], 29 | } 30 | 31 | 32 | def test_can_serialise_a_minimal_doc_to_dict(minimal_doc, min_doc_owner_pub_key): 33 | dict_doc = minimal_doc.to_dict() 34 | assert dict_doc.pop('updateTime') == minimal_doc.update_time 35 | 36 | assert dict_doc == {'@context': 'https://w3id.org/did/v1', 37 | 'id': minimal_doc.did, 38 | 'ioticsDIDType': minimal_doc.purpose.value, 39 | 'ioticsSpecVersion': minimal_doc.spec_version, 40 | 'metadata': Metadata().to_dict(), 41 | 'revoked': minimal_doc.revoked, 42 | 'proof': minimal_doc.proof, 43 | 'publicKey': [min_doc_owner_pub_key.to_dict()], 44 | 'authentication': [], 45 | 'delegateControl': [], 46 | 'delegateAuthentication': [], 47 | } 48 | 49 | 50 | def test_can_build_doc_metadata(): 51 | label = 'a label' 52 | comment = 'a comment' 53 | url = 'http://an/url' 54 | metadata = Metadata.build(label, comment, url) 55 | assert metadata.label == label 56 | assert metadata.comment == comment 57 | assert metadata.url == url 58 | assert metadata.to_dict() == {'label': label, 59 | 'comment': comment, 60 | 'url': url} 61 | 62 | 63 | def test_can_build_doc_metadata_from_dict(): 64 | data = {'label': 'a label', 65 | 'comment': 'a comment', 66 | 'url': 'http://an/url'} 67 | metadata = Metadata.from_dict(data) 68 | assert metadata.label == data['label'] 69 | assert metadata.comment == data['comment'] 70 | assert metadata.url == data['url'] 71 | 72 | 73 | @pytest.mark.parametrize('invalid_input', ({'label': 'a' * (DOCUMENT_MAX_LABEL_LENGTH + 1)}, 74 | {'comment': 'a' * (DOCUMENT_MAX_COMMENT_LENGTH + 1)}, 75 | {'url': 'a' * (DOCUMENT_MAX_URL_LENGTH + 1)})) 76 | def test_build_doc_metadata_raises_validaion_error_in_invalid_data(invalid_input): 77 | params = {'label': 'a label', 'comment': 'a comment', 'url': 'http://an/url'} 78 | params.update(invalid_input) 79 | with pytest.raises(IdentityValidationError): 80 | Metadata.build(**params) 81 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/register/test_keys.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity import DelegationProofType 6 | from iotics.lib.identity.const import DOCUMENT_AUTHENTICATION_TYPE, DOCUMENT_PUBLIC_KEY_TYPE 7 | from iotics.lib.identity.error import IdentityValidationError 8 | from iotics.lib.identity.register.keys import RegisterAuthenticationPublicKey, RegisterDelegationProof, \ 9 | RegisterPublicKey 10 | from tests.unit.iotics.lib.identity.register.conftest import get_public_base_58_key 11 | 12 | 13 | def test_can_build_register_public_key(valid_key_name, valid_public_key_base58): 14 | key = RegisterPublicKey.build(valid_key_name, valid_public_key_base58, revoked=False) 15 | assert key.name == valid_key_name 16 | assert key.base58 == valid_public_key_base58 17 | assert not key.revoked 18 | 19 | assert key.to_dict() == {'id': valid_key_name, 20 | 'type': DOCUMENT_PUBLIC_KEY_TYPE, 21 | 'publicKeyBase58': valid_public_key_base58, 22 | 'revoked': False} 23 | 24 | 25 | def test_build_register_public_key_raises_validation_error_if_invalid_name(valid_public_key_base58): 26 | with pytest.raises(IdentityValidationError): 27 | RegisterPublicKey.build('Invalid_name', valid_public_key_base58, revoked=False) 28 | 29 | 30 | def test_build_register_public_key_from_dict_raises_validation_error_if_invalid_dict(): 31 | with pytest.raises(IdentityValidationError): 32 | RegisterPublicKey.from_dict({'invalid': 'data'}) 33 | 34 | 35 | def test_can_build_register_auth_key(valid_key_name, valid_public_key_base58): 36 | key = RegisterAuthenticationPublicKey.build(valid_key_name, valid_public_key_base58, revoked=False) 37 | assert key.name == valid_key_name 38 | assert key.base58 == valid_public_key_base58 39 | assert not key.revoked 40 | 41 | assert key.to_dict() == {'id': valid_key_name, 42 | 'type': DOCUMENT_AUTHENTICATION_TYPE, 43 | 'publicKeyBase58': valid_public_key_base58, 44 | 'revoked': False} 45 | 46 | 47 | def test_build_register_auth_key_raises_validaion_error_if_invalid_name(valid_public_key_base58): 48 | with pytest.raises(IdentityValidationError): 49 | RegisterAuthenticationPublicKey.build('Invalid_name', valid_public_key_base58, revoked=False) 50 | 51 | 52 | def test_build_register_auth_key_from_dict_raises_validation_error_if_invalid_dict(): 53 | with pytest.raises(IdentityValidationError): 54 | RegisterAuthenticationPublicKey.from_dict({'invalid': 'data'}) 55 | 56 | 57 | @pytest.mark.parametrize('proof_type', list(DelegationProofType)) 58 | def test_can_build_register_delegation_proof(valid_key_name, a_proof, a_controller, proof_type): 59 | key = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 60 | proof=a_proof, revoked=False, proof_type=proof_type) 61 | assert key.name == valid_key_name 62 | assert key.controller == a_controller 63 | assert key.proof == a_proof 64 | assert not key.revoked 65 | 66 | assert key.to_dict() == {'id': valid_key_name, 67 | 'controller': str(a_controller), 68 | 'proof': a_proof, 69 | 'proofType': proof_type.value, 70 | 'revoked': False} 71 | 72 | 73 | def test_build_register_delegation_proof_raises_validation_error_if_invalid_name(a_proof, a_controller): 74 | with pytest.raises(IdentityValidationError): 75 | RegisterDelegationProof.build(name='InvalidName', controller=a_controller, proof=a_proof, revoked=False) 76 | 77 | 78 | def test_build_register_delegation_proof_from_dict_raises_validation_error_if_invalid_dict(): 79 | with pytest.raises(IdentityValidationError): 80 | RegisterDelegationProof.from_dict({'invalid': 'data'}) 81 | 82 | 83 | @pytest.mark.parametrize('proof_type', list(DelegationProofType)) 84 | def test_can_build_register_delegation_proof_from_dict(valid_key_name, a_proof, a_controller, proof_type): 85 | key = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 86 | proof=a_proof, revoked=False, proof_type=proof_type) 87 | from_dict_key = RegisterDelegationProof.from_dict(key.to_dict()) 88 | assert key == from_dict_key 89 | 90 | 91 | def test_can_build_register_delegation_proof_from_dict_with_missing_proof_type(valid_key_name, a_proof, a_controller): 92 | key = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 93 | proof=a_proof, revoked=False) 94 | as_dict = key.to_dict() 95 | as_dict.pop('proofType') 96 | from_dict_key = RegisterDelegationProof.from_dict(as_dict) 97 | assert from_dict_key.proof_type == DelegationProofType.DID 98 | 99 | 100 | def test_build_register_delegation_proof_from_dict_raises_validation_error_if_invalid_proof_type(valid_key_name, 101 | a_proof, 102 | a_controller): 103 | key = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 104 | proof=a_proof, revoked=False) 105 | as_dict = key.to_dict() 106 | as_dict['proofType'] = 'invalid' 107 | with pytest.raises(IdentityValidationError): 108 | RegisterDelegationProof.from_dict(as_dict) 109 | 110 | 111 | def test_is_equal_register_delegation_proof(valid_key_name, a_proof, a_controller): 112 | key1 = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 113 | proof=a_proof, revoked=False) 114 | key2 = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 115 | proof='difference_ignored', revoked=False) 116 | assert key1.is_equal(key2) 117 | 118 | 119 | def test_not_is_equal_register_delegation_proof(valid_key_name, a_proof, a_controller, b_controller): 120 | key1 = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 121 | proof=a_proof, revoked=False) 122 | key2 = RegisterDelegationProof(name=valid_key_name, controller=b_controller, 123 | proof='difference_ignored', revoked=False) 124 | assert not key1.is_equal(key2) 125 | 126 | 127 | def test_not_is_equal_register_delegation_proof_type(valid_key_name, a_proof, a_controller): 128 | key1 = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 129 | proof=a_proof, revoked=False, proof_type=DelegationProofType.DID) 130 | key2 = RegisterDelegationProof(name=valid_key_name, controller=a_controller, 131 | proof='difference_ignored', revoked=False, proof_type=DelegationProofType.GENERIC) 132 | assert not key1.is_equal(key2) 133 | 134 | 135 | def test_is_equal_register_public_key(valid_key_name, valid_public_key_base58): 136 | key1 = RegisterPublicKey.build(valid_key_name, valid_public_key_base58, revoked=False) 137 | key2 = RegisterPublicKey.build(valid_key_name, valid_public_key_base58, revoked=False) 138 | assert key1.is_equal(key2) 139 | 140 | 141 | def test_not_is_equal_register_public_key(valid_key_name, valid_public_key_base58): 142 | key1 = RegisterPublicKey.build(valid_key_name, valid_public_key_base58, revoked=False) 143 | key2 = RegisterPublicKey.build(valid_key_name, get_public_base_58_key(), revoked=False) 144 | assert not key1.is_equal(key2) 145 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/validation/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/validation/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | from typing import Callable, Tuple 4 | 5 | import base58 6 | 7 | from iotics.lib.identity import DelegationProofType, RegisterDocument, APIProof 8 | from iotics.lib.identity.api.advanced_api import AdvancedIdentityLocalApi 9 | from iotics.lib.identity.crypto.identity import make_identifier 10 | from iotics.lib.identity.crypto.issuer import Issuer 11 | from iotics.lib.identity.crypto.key_pair_secrets import DIDType, KeyPairSecrets, KeyPairSecretsHelper 12 | from iotics.lib.identity.crypto.proof import Proof 13 | from iotics.lib.identity.register.document_builder import RegisterDocumentBuilder 14 | from iotics.lib.identity.register.keys import RegisterDelegationProof, RegisterPublicKey 15 | 16 | 17 | def new_seed(length: int = 128) -> bytes: 18 | return AdvancedIdentityLocalApi.create_seed(length) 19 | 20 | 21 | def is_validator_run_success(validator: Callable, *args, **kwargs): 22 | """ Run a validation helper and ensure it has been run. 23 | By design the validators return nothing and they raise when something is invalid. 24 | For the valid case we want to highlight the fact the validator has been called returning True 25 | at the end of this helper.""" 26 | validator(*args, **kwargs) 27 | return True 28 | 29 | 30 | def get_delegation_proof(issuer: Issuer, key_pair_secrets: KeyPairSecrets, delegating_doc_id: str) -> APIProof: 31 | return APIProof.build(key_pair_secrets, issuer, content=delegating_doc_id.encode()) 32 | 33 | 34 | def get_delegation_register_proof(subject_key_pair_secrets: KeyPairSecrets, 35 | subject_issuer: Issuer, 36 | content: bytes, 37 | p_type: DelegationProofType, 38 | deleg_key_name='#DelegKey') -> RegisterDelegationProof: 39 | proof = Proof.build(subject_key_pair_secrets, subject_issuer, content=content) 40 | 41 | return RegisterDelegationProof.build(deleg_key_name, 42 | controller=subject_issuer, 43 | proof=proof.signature, 44 | p_type=p_type, 45 | revoked=False) 46 | 47 | 48 | def get_valid_document(seed: bytes, issuer_name: str, controller: Issuer = None): 49 | secrets = KeyPairSecrets.build(seed, 'iotics/0/something/twin') 50 | return get_valid_document_from_secret(secrets, issuer_name, controller) 51 | 52 | 53 | def get_new_document(issuer_name: str) -> Tuple[KeyPairSecrets, Issuer, RegisterDocument]: 54 | secrets = KeyPairSecrets.build(new_seed(), 'iotics/0/something') 55 | doc = get_valid_document_from_secret(secrets, issuer_name) 56 | return secrets, Issuer.build(doc.did, issuer_name), doc 57 | 58 | 59 | def get_valid_document_from_secret(secrets: KeyPairSecrets, issuer_name: str, controller: Issuer = None): 60 | public_base58 = KeyPairSecretsHelper.get_public_key_base58_from_key_pair_secrets(secrets) 61 | public_bytes = base58.b58decode(public_base58) 62 | doc_id = make_identifier(public_bytes) 63 | proof = Proof.build(secrets, Issuer.build(doc_id, issuer_name), content=doc_id.encode()) 64 | return RegisterDocumentBuilder() \ 65 | .add_public_key_obj(RegisterPublicKey(issuer_name, public_base58, revoked=False)) \ 66 | .build(doc_id, 67 | DIDType.TWIN, 68 | proof=proof.signature, 69 | revoked=False, 70 | controller=controller) 71 | 72 | 73 | def get_valid_delegated_doc_and_deleg_proof(seed: bytes, issuer_name: str, delegating_doc_id: str, deleg_name: str): 74 | secrets = KeyPairSecrets.build(seed, 'iotics/0/something/twindeleg') 75 | public_base58 = KeyPairSecretsHelper.get_public_key_base58_from_key_pair_secrets(secrets) 76 | public_bytes = base58.b58decode(public_base58) 77 | doc_id = make_identifier(public_bytes) 78 | issuer = Issuer.build(doc_id, issuer_name) 79 | proof = Proof.build(secrets, issuer, content=doc_id.encode()) 80 | 81 | deleg_key = get_delegation_register_proof(subject_key_pair_secrets=secrets, 82 | content=delegating_doc_id.encode(), 83 | p_type=DelegationProofType.DID, 84 | subject_issuer=Issuer.build(doc_id, issuer_name), 85 | deleg_key_name=deleg_name) 86 | delegated_doc = RegisterDocumentBuilder() \ 87 | .add_public_key_obj(RegisterPublicKey(issuer_name, public_base58, revoked=False)) \ 88 | .build(doc_id, DIDType.TWIN, proof=proof.signature, revoked=False) 89 | return delegated_doc, deleg_key 90 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/validation/test_document.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.crypto.identity import make_identifier 6 | from iotics.lib.identity.crypto.key_pair_secrets import DIDType, KeyPairSecretsHelper 7 | from iotics.lib.identity.crypto.proof import Proof 8 | from iotics.lib.identity.error import IdentityInvalidDocumentError, \ 9 | IdentityInvalidProofError, IdentityResolverError 10 | from iotics.lib.identity.register.document_builder import RegisterDocumentBuilder 11 | from iotics.lib.identity.register.keys import RegisterPublicKey 12 | from iotics.lib.identity.validation.document import DocumentValidation 13 | from tests.unit.iotics.lib.identity.fake import ResolverClientTest 14 | from tests.unit.iotics.lib.identity.helper import get_doc_with_keys 15 | from tests.unit.iotics.lib.identity.validation.helper import get_valid_delegated_doc_and_deleg_proof, \ 16 | get_valid_document_from_secret, is_validator_run_success, new_seed 17 | 18 | 19 | @pytest.fixture 20 | def doc_invalid_proof(valid_key_pair_secrets, valid_issuer): 21 | return Proof.build(valid_key_pair_secrets, valid_issuer, content='not the doc id'.encode()) 22 | 23 | 24 | @pytest.fixture 25 | def valid_doc(valid_issuer, valid_key_pair_secrets): 26 | return get_valid_document_from_secret(valid_key_pair_secrets, valid_issuer.name) 27 | 28 | 29 | @pytest.fixture 30 | def invalid_doc_no_owner_key(valid_doc, valid_issuer, valid_key_pair_secrets, other_key_pair_secrets): 31 | public_base58 = KeyPairSecretsHelper.get_public_key_base58_from_key_pair_secrets(other_key_pair_secrets) 32 | doc_id = valid_doc.did 33 | return RegisterDocumentBuilder() \ 34 | .add_public_key_obj(RegisterPublicKey('#KeyNotFromOwner', public_base58, revoked=False)) \ 35 | .build(doc_id, 36 | DIDType.TWIN, 37 | proof=Proof.build(valid_key_pair_secrets, valid_issuer, content=doc_id.encode()).signature, 38 | revoked=False) 39 | 40 | 41 | @pytest.fixture 42 | def invalid_doc_invalid_proof(valid_doc, doc_invalid_proof, valid_key_pair_secrets): 43 | public_base58 = KeyPairSecretsHelper.get_public_key_base58_from_key_pair_secrets(valid_key_pair_secrets) 44 | doc_id = valid_doc.did 45 | return RegisterDocumentBuilder() \ 46 | .add_public_key_obj(RegisterPublicKey('#Owner', public_base58, revoked=False)) \ 47 | .build(doc_id, 48 | DIDType.TWIN, 49 | proof=doc_invalid_proof.signature, 50 | revoked=False) 51 | 52 | 53 | @pytest.fixture 54 | def invalid_doc(doc_did, valid_issuer_key): 55 | return get_doc_with_keys( 56 | did=doc_did, 57 | public_keys=[RegisterPublicKey.build('#Key1', valid_issuer_key.public_key_base58, revoked=False), ] 58 | ) 59 | 60 | 61 | def test_can_validate_document_proof(valid_doc): 62 | assert is_validator_run_success(DocumentValidation.validate_new_document_proof, valid_doc) 63 | 64 | 65 | def test_validate_document_proof_fails_if_no_owner_key(invalid_doc_no_owner_key): 66 | with pytest.raises(IdentityInvalidDocumentError): 67 | is_validator_run_success(DocumentValidation.validate_new_document_proof, invalid_doc_no_owner_key) 68 | 69 | 70 | def test_validate_document_proof_fails_if_invalid_proof(invalid_doc_invalid_proof): 71 | with pytest.raises(IdentityInvalidDocumentError) as err_wrapper: 72 | is_validator_run_success(DocumentValidation.validate_new_document_proof, invalid_doc_invalid_proof) 73 | assert isinstance(err_wrapper.value.__cause__, IdentityInvalidProofError) 74 | 75 | 76 | def test_can_validate_document_without_delegation_against_resolver(valid_doc): 77 | resolver_client = ResolverClientTest(docs={}) 78 | assert is_validator_run_success(DocumentValidation.validate_document_against_resolver, 79 | resolver_client, valid_doc) 80 | 81 | 82 | def test_can_validate_document_with_delegation_against_resolver(valid_doc): 83 | delegated_doc1, deleg_key1 = get_valid_delegated_doc_and_deleg_proof(new_seed(), '#issuer1', 84 | delegating_doc_id=valid_doc.did, 85 | deleg_name='#DelegDoc1') 86 | delegated_doc2, deleg_key2 = get_valid_delegated_doc_and_deleg_proof(new_seed(), '#issuer2', 87 | delegating_doc_id=valid_doc.did, 88 | deleg_name='#DelegDoc2') 89 | 90 | valid_doc = RegisterDocumentBuilder() \ 91 | .add_control_delegation_obj(deleg_key1) \ 92 | .add_control_delegation_obj(deleg_key2) \ 93 | .add_authentication_delegation(deleg_key1.name + 'auth', deleg_key1.controller, deleg_key1.proof, 94 | deleg_key1.revoked) \ 95 | .add_authentication_delegation(deleg_key2.name + 'auth', deleg_key2.controller, deleg_key2.proof, 96 | deleg_key2.revoked) \ 97 | .build_from_existing(valid_doc) 98 | resolver_client = ResolverClientTest(docs={valid_doc.did: valid_doc, 99 | delegated_doc1.did: delegated_doc1, 100 | delegated_doc2.did: delegated_doc2}) 101 | assert is_validator_run_success(DocumentValidation.validate_document_against_resolver, 102 | resolver_client, valid_doc) 103 | 104 | 105 | def test_can_validate_new_document_proof(valid_doc): 106 | assert is_validator_run_success(DocumentValidation.validate_new_document_proof, valid_doc) 107 | 108 | 109 | def test_validate_document_proof_raises_validation_error_if_invalid_doc_proof(invalid_doc_invalid_proof): 110 | with pytest.raises(IdentityInvalidDocumentError) as err_wrapper: 111 | is_validator_run_success(DocumentValidation.validate_new_document_proof, invalid_doc_invalid_proof) 112 | assert isinstance(err_wrapper.value.__cause__, IdentityInvalidProofError) 113 | 114 | 115 | @pytest.mark.parametrize('deleg_type', ('auth', 'control')) 116 | def test_validate_document_against_resolver_raises_validation_error_if_invalid_delegation(valid_doc, other_key_pair, 117 | deleg_type): 118 | wrong_deleg_id = make_identifier(other_key_pair.public_bytes) 119 | delegated_doc1, inconsistent_deleg_key = get_valid_delegated_doc_and_deleg_proof(new_seed(), '#issuer1', 120 | delegating_doc_id=wrong_deleg_id, 121 | deleg_name='#DelegDoc1') 122 | 123 | builder = RegisterDocumentBuilder() 124 | if deleg_type == 'auth': 125 | builder.add_authentication_delegation_obj(inconsistent_deleg_key) 126 | else: 127 | builder.add_control_delegation_obj(inconsistent_deleg_key) 128 | doc_with_invalid_delegation = builder.build_from_existing(valid_doc) 129 | resolver_client = ResolverClientTest(docs={valid_doc.did: valid_doc, 130 | delegated_doc1.did: delegated_doc1}) 131 | with pytest.raises(IdentityInvalidDocumentError) as err_wrapper: 132 | is_validator_run_success(DocumentValidation.validate_document_against_resolver, resolver_client, 133 | doc_with_invalid_delegation) 134 | assert isinstance(err_wrapper.value.__cause__, IdentityInvalidProofError) 135 | 136 | 137 | def test_validate_document_against_resolver_raises_validation_error_if_resolver_error(valid_doc): 138 | _, deleg_key = get_valid_delegated_doc_and_deleg_proof(new_seed(), '#issuer1', 139 | delegating_doc_id=valid_doc.did, 140 | deleg_name='#DelegDoc1') 141 | 142 | valid_doc = RegisterDocumentBuilder() \ 143 | .add_control_delegation_obj(deleg_key) \ 144 | .build_from_existing(valid_doc) 145 | 146 | # Initialised without the delegation doc so a not found will be raised 147 | resolver_client = ResolverClientTest(docs={valid_doc.did: valid_doc}) 148 | with pytest.raises(IdentityInvalidDocumentError) as err_wrapper: 149 | is_validator_run_success(DocumentValidation.validate_document_against_resolver, resolver_client, 150 | valid_doc) 151 | assert isinstance(err_wrapper.value.__cause__, IdentityResolverError) 152 | -------------------------------------------------------------------------------- /tests/unit/iotics/lib/identity/validation/test_identifier.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) IOTIC LABS LIMITED. All rights reserved. Licensed under the Apache License, Version 2.0. 2 | 3 | import pytest 4 | 5 | from iotics.lib.identity.error import IdentityValidationError 6 | from iotics.lib.identity.validation.identity import IdentityValidation 7 | from tests.unit.iotics.lib.identity.validation.helper import is_validator_run_success 8 | 9 | 10 | @pytest.fixture 11 | def an_identifier(): 12 | return 'did:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEgY' 13 | 14 | 15 | def test_validate_identifier_do_not_raises_if_valid_identifier(an_identifier): 16 | assert is_validator_run_success(IdentityValidation.validate_identifier, an_identifier) 17 | 18 | 19 | @pytest.mark.parametrize('invalid_identifier', ( 20 | 'ddi:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEgY', # Invalid prefix 21 | 'did:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEI', # Invalid 'I' character 22 | 'did:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEg', # Invalid size 23 | )) 24 | def test_validate_identifier_raises_validation_error_if_invalid_identifier(invalid_identifier): 25 | with pytest.raises(IdentityValidationError): 26 | is_validator_run_success(IdentityValidation.validate_identifier, invalid_identifier) 27 | 28 | 29 | @pytest.mark.parametrize('key_name', ('#AName', 30 | '#a', 31 | '#b-C-8')) 32 | def test_validate_key_name_do_not_raises_if_valid_identifier(key_name): 33 | assert is_validator_run_success(IdentityValidation.validate_key_name, key_name) 34 | 35 | 36 | @pytest.mark.parametrize('invalid_key_name', ('AName', # Invalid prefix 37 | '#' + 'a' * 50, # Too long 38 | '#a+plop', # Invalid char 39 | )) 40 | def test_validate_key_name_raises_validation_error_if_invalid_identifier(invalid_key_name): 41 | with pytest.raises(IdentityValidationError): 42 | is_validator_run_success(IdentityValidation.validate_key_name, invalid_key_name) 43 | 44 | 45 | def test_validate_issuer_string_do_not_raises_if_valid_issuer(an_identifier): 46 | assert is_validator_run_success(IdentityValidation.validate_issuer_string, f'{an_identifier}#AName') 47 | 48 | 49 | @pytest.mark.parametrize('invalid_issuer', 50 | ('ddi:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEgY#AName', # Invalid identifier 51 | 'did:iotics:iotHHHHKpPGWyEC4FFo4d6oyzVVk6MXLmEY#++A++', # Invalid Name 52 | )) 53 | def test_validate_issuer_string_raises_validation_error_if_invalid_identifier(invalid_issuer): 54 | with pytest.raises(IdentityValidationError): 55 | is_validator_run_success(IdentityValidation.validate_identifier, invalid_issuer) 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = lint,pytest,pytestbdd,mypy 3 | skipsdist = True 4 | 5 | 6 | [testenv:lint] 7 | deps = 8 | -e .[lint,test] 9 | 10 | commands = 11 | flake8 iotics setup.py tests 12 | pylint --rcfile .pylint.rc iotics setup.py 13 | pylint --rcfile .pylint.rc tests --disable=redefined-outer-name 14 | 15 | 16 | 17 | [testenv:mypy] 18 | deps = 19 | -e .[lint] 20 | 21 | commands = 22 | mypy --config-file setup.cfg iotics 23 | 24 | [testenv:pytest] 25 | deps = 26 | -e .[test] 27 | 28 | setenv = 29 | PYTHONDONTWRITEBYTECODE = 1 30 | 31 | commands = 32 | pytest \ 33 | --tb=short \ 34 | --junitxml=test-unit-results.xml \ 35 | --cov=iotics --cov-report=term --cov-report=xml --cov-report=html \ 36 | tests/unit {posargs} 37 | 38 | 39 | 40 | [testenv:pytestbdd] 41 | deps = 42 | -e .[test] 43 | 44 | setenv = 45 | PYTHONDONTWRITEBYTECODE = 1 46 | 47 | commands = 48 | pytest \ 49 | --tb=short \ 50 | --junitxml=test-bdd-results.xml \ 51 | --cov=iotics --cov-report=term --cov-report=xml --cov-report=html \ 52 | tests/behaviour {posargs} 53 | 54 | [pytest] 55 | # see https://docs.pytest.org/en/latest/deprecations.html#junit-family-default-value-change-to-xunit2 56 | junit_family = xunit2 57 | log_cli = 1 58 | 59 | [coverage:run] 60 | branch = True 61 | 62 | [report] 63 | show_missing = True 64 | --------------------------------------------------------------------------------