├── .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 | [](https://pypi.org/project/iotics-identity)
4 | [](https://pypi.org/project/iotics-identity/#files)
5 | [](https://github.com/Iotic-Labs/iotics-identity-py/blob/main/LICENSE)
6 | [](https://github.com/Iotic-Labs/iotics-identity-py/issues)
7 | [](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 [](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 |
--------------------------------------------------------------------------------