sep2tools 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sep2tools-0.1.0/.github/workflows/pythonpackage.yml +38 -0
- sep2tools-0.1.0/.gitignore +165 -0
- sep2tools-0.1.0/LICENSE +21 -0
- sep2tools-0.1.0/PKG-INFO +27 -0
- sep2tools-0.1.0/README.md +2 -0
- sep2tools-0.1.0/pyproject.toml +41 -0
- sep2tools-0.1.0/sep2tools/__init__.py +13 -0
- sep2tools-0.1.0/sep2tools/cert_create.py +175 -0
- sep2tools-0.1.0/sep2tools/cert_id.py +87 -0
- sep2tools-0.1.0/sep2tools/hexmaps.py +177 -0
- sep2tools-0.1.0/sep2tools/ids.py +37 -0
- sep2tools-0.1.0/sep2tools/version.py +1 -0
- sep2tools-0.1.0/tests/__init__.py +0 -0
- sep2tools-0.1.0/tests/test_cert_create.py +24 -0
- sep2tools-0.1.0/tests/test_cert_ids.py +34 -0
- sep2tools-0.1.0/tests/test_example_cert.py +15 -0
- sep2tools-0.1.0/tests/test_hexmaps.py +71 -0
- sep2tools-0.1.0/tests/test_ids.py +27 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Python package
|
|
2
|
+
|
|
3
|
+
on: ["pull_request"]
|
|
4
|
+
jobs:
|
|
5
|
+
build:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
strategy:
|
|
8
|
+
fail-fast: false
|
|
9
|
+
matrix:
|
|
10
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v3
|
|
13
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
14
|
+
uses: actions/setup-python@v4
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: |
|
|
19
|
+
python -m pip install --upgrade pip wheel
|
|
20
|
+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
21
|
+
# install the local folder
|
|
22
|
+
python -m pip install -e .
|
|
23
|
+
- name: Test with ruff
|
|
24
|
+
run: |
|
|
25
|
+
pip install ruff
|
|
26
|
+
ruff check .
|
|
27
|
+
- name: Test with pytest
|
|
28
|
+
run: |
|
|
29
|
+
pip install pytest pytest-cov
|
|
30
|
+
pytest --cov-report=xml
|
|
31
|
+
- name: Coveralls
|
|
32
|
+
env:
|
|
33
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
34
|
+
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
|
35
|
+
run: |
|
|
36
|
+
pip install coveralls
|
|
37
|
+
coverage run --source=sep2tools -m pytest tests/
|
|
38
|
+
coveralls
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
110
|
+
.pdm.toml
|
|
111
|
+
.pdm-python
|
|
112
|
+
.pdm-build/
|
|
113
|
+
|
|
114
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
115
|
+
__pypackages__/
|
|
116
|
+
|
|
117
|
+
# Celery stuff
|
|
118
|
+
celerybeat-schedule
|
|
119
|
+
celerybeat.pid
|
|
120
|
+
|
|
121
|
+
# SageMath parsed files
|
|
122
|
+
*.sage.py
|
|
123
|
+
|
|
124
|
+
# Environments
|
|
125
|
+
.env
|
|
126
|
+
.venv
|
|
127
|
+
env/
|
|
128
|
+
venv/
|
|
129
|
+
ENV/
|
|
130
|
+
env.bak/
|
|
131
|
+
venv.bak/
|
|
132
|
+
|
|
133
|
+
# Spyder project settings
|
|
134
|
+
.spyderproject
|
|
135
|
+
.spyproject
|
|
136
|
+
|
|
137
|
+
# Rope project settings
|
|
138
|
+
.ropeproject
|
|
139
|
+
|
|
140
|
+
# mkdocs documentation
|
|
141
|
+
/site
|
|
142
|
+
|
|
143
|
+
# mypy
|
|
144
|
+
.mypy_cache/
|
|
145
|
+
.dmypy.json
|
|
146
|
+
dmypy.json
|
|
147
|
+
|
|
148
|
+
# Pyre type checker
|
|
149
|
+
.pyre/
|
|
150
|
+
|
|
151
|
+
# pytype static type analyzer
|
|
152
|
+
.pytype/
|
|
153
|
+
|
|
154
|
+
# Cython debug symbols
|
|
155
|
+
cython_debug/
|
|
156
|
+
|
|
157
|
+
# PyCharm
|
|
158
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
159
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
160
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
161
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
162
|
+
#.idea/
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
certs/
|
sep2tools-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alex Guinman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sep2tools-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sep2tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Useful functions for working with IEEE 2030.5 (SEP2)
|
|
5
|
+
Author-email: Alex Guinman <alex@guinman.id.au>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Dist: cryptography
|
|
16
|
+
Requires-Dist: asn1
|
|
17
|
+
Requires-Dist: python-dateutil
|
|
18
|
+
Requires-Dist: ruff ; extra == "test"
|
|
19
|
+
Requires-Dist: pytest >=2.7.3 ; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-cov ; extra == "test"
|
|
21
|
+
Requires-Dist: mypy ; extra == "test"
|
|
22
|
+
Project-URL: Source, https://github.com/aguinane/SEP2-Tools
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
|
|
25
|
+
# SEP2-Tools
|
|
26
|
+
This library provides some useful functions for working with IEEE 2030.5 (SEP2).
|
|
27
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sep2tools"
|
|
7
|
+
authors = [{ name = "Alex Guinman", email = "alex@guinman.id.au" }]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { file = "LICENSE" }
|
|
10
|
+
classifiers = [
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.9"
|
|
20
|
+
dynamic = ["version", "description"]
|
|
21
|
+
dependencies = ["cryptography", "asn1", "python-dateutil"]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = ["ruff", "pytest >=2.7.3", "pytest-cov", "mypy"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Source = "https://github.com/aguinane/SEP2-Tools"
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
addopts = "-ra --failed-first --showlocals --durations=3 --cov=sep2tools"
|
|
31
|
+
|
|
32
|
+
[tool.coverage.run]
|
|
33
|
+
omit = ["*/version.py", '*/__main__.py']
|
|
34
|
+
|
|
35
|
+
[tool.coverage.report]
|
|
36
|
+
show_missing = true
|
|
37
|
+
skip_empty = true
|
|
38
|
+
fail_under = 80
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["A", "B", "E", "F", "I", "N", "SIM", "UP"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
""" Useful functions for working with IEEE 2030.5 (SEP2)"""
|
|
2
|
+
|
|
3
|
+
from .cert_id import get_der_certificate_lfdi, get_pem_certificate_lfdi
|
|
4
|
+
from .ids import generate_mrid, proxy_device_lfdi
|
|
5
|
+
from .version import __version__
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"__version__",
|
|
9
|
+
"get_pem_certificate_lfdi",
|
|
10
|
+
"get_der_certificate_lfdi",
|
|
11
|
+
"proxy_device_lfdi",
|
|
12
|
+
"generate_mrid",
|
|
13
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import asn1
|
|
7
|
+
from cryptography import x509
|
|
8
|
+
from cryptography.hazmat.primitives import serialization
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
10
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
11
|
+
from cryptography.x509.oid import ObjectIdentifier
|
|
12
|
+
from dateutil import tz
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
DEFAULT_KEY_PATH = Path("key.pem")
|
|
17
|
+
DEFAULT_CSR_PATH = Path("csr.pem")
|
|
18
|
+
|
|
19
|
+
# IEEE 2030.5 device type assignments (Section 6.11.7.2)
|
|
20
|
+
SEP2_DEV_GENERIC = ObjectIdentifier("1.3.6.1.4.1.40732.1.1")
|
|
21
|
+
SEP2_DEV_MOBILE = ObjectIdentifier("1.3.6.1.4.1.40732.1.2")
|
|
22
|
+
SEP2_DEV_POSTMANUF = ObjectIdentifier("1.3.6.1.4.1.40732.1.3")
|
|
23
|
+
|
|
24
|
+
# IEEE 2030.5 policy assignments (Section 6.11.7.3)
|
|
25
|
+
SEP2_TEST_CERT = ObjectIdentifier("1.3.6.1.4.1.40732.2.1")
|
|
26
|
+
SEP2_TEST_SELFSIGNED = ObjectIdentifier("1.3.6.1.4.1.40732.2.2")
|
|
27
|
+
SEP2_TEST_SERVPROV = ObjectIdentifier("1.3.6.1.4.1.40732.2.3")
|
|
28
|
+
SEP2_TEST_BULK_CERT = ObjectIdentifier("1.3.6.1.4.1.40732.2.4")
|
|
29
|
+
|
|
30
|
+
# HardwareModuleName (Section 6.11.7.4)
|
|
31
|
+
SEP2_HARDWARE_MODULE_NAME = ObjectIdentifier("1.3.6.1.5.5.7.8.4")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def generate_key_and_csr(
|
|
35
|
+
key_file: Path = DEFAULT_KEY_PATH, csr_file: Path = DEFAULT_CSR_PATH
|
|
36
|
+
) -> tuple[Path, Path]:
|
|
37
|
+
"""Generate a Private Key and Certificate Signing Request (CSR)"""
|
|
38
|
+
key = ec.generate_private_key(ec.SECP256R1)
|
|
39
|
+
key_pem = key.private_bytes(
|
|
40
|
+
encoding=serialization.Encoding.PEM,
|
|
41
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
42
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
43
|
+
)
|
|
44
|
+
with open(key_file, "wb") as fh:
|
|
45
|
+
fh.write(key_pem)
|
|
46
|
+
log.info("Created Key at %s", key_file)
|
|
47
|
+
|
|
48
|
+
subject_name = ""
|
|
49
|
+
csr = (
|
|
50
|
+
x509.CertificateSigningRequestBuilder()
|
|
51
|
+
.subject_name(x509.Name(subject_name))
|
|
52
|
+
.sign(key, SHA256())
|
|
53
|
+
)
|
|
54
|
+
csr_pem = csr.public_bytes(serialization.Encoding.PEM)
|
|
55
|
+
with open(csr_file, "wb") as fh:
|
|
56
|
+
fh.write(csr_pem)
|
|
57
|
+
log.info("Created CSR at %s", csr_file)
|
|
58
|
+
return key_file, csr_file
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def convert_pem_to_der(filename: Path) -> Path:
|
|
62
|
+
"""Convert a PEM file to DER Certificate"""
|
|
63
|
+
with open(filename, "rb") as pem_file:
|
|
64
|
+
cert_data = pem_file.read()
|
|
65
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
|
66
|
+
der_data = cert.public_bytes(encoding=serialization.Encoding.DER)
|
|
67
|
+
output_der_path = filename.with_suffix(".der")
|
|
68
|
+
with open(output_der_path, "wb") as fh:
|
|
69
|
+
fh.write(der_data)
|
|
70
|
+
return output_der_path
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def generate_device_certificate(
|
|
74
|
+
filename: Path,
|
|
75
|
+
csr_path: Path,
|
|
76
|
+
mica_cert_path: Path,
|
|
77
|
+
mica_key_path: Path,
|
|
78
|
+
hardware_type_oid: str,
|
|
79
|
+
hardware_serial_number: str,
|
|
80
|
+
policy_oids: list[ObjectIdentifier],
|
|
81
|
+
serca_cert_path: Optional[Path] = None,
|
|
82
|
+
) -> Path:
|
|
83
|
+
"""Use a CSR and MICA key pair to generate a SEP2 Certificate"""
|
|
84
|
+
|
|
85
|
+
with open(csr_path, "rb") as pem_file:
|
|
86
|
+
csr_data = pem_file.read()
|
|
87
|
+
csr = x509.load_pem_x509_csr(csr_data)
|
|
88
|
+
with open(mica_cert_path, "rb") as pem_file:
|
|
89
|
+
mica_pem_data = pem_file.read()
|
|
90
|
+
mica_cert = x509.load_pem_x509_certificate(mica_pem_data)
|
|
91
|
+
with open(mica_key_path, "rb") as pem_file:
|
|
92
|
+
pem_data = pem_file.read()
|
|
93
|
+
mica_key = serialization.load_pem_private_key(pem_data, password=None)
|
|
94
|
+
valid_from = datetime.now(tz=tz.UTC)
|
|
95
|
+
valid_to = datetime(9999, 12, 31, 23, 59, 59, 0) # as per standard
|
|
96
|
+
|
|
97
|
+
policies = [x509.PolicyInformation(oid, None) for oid in policy_oids]
|
|
98
|
+
|
|
99
|
+
# SAN OtherName encoder
|
|
100
|
+
encoder = asn1.Encoder()
|
|
101
|
+
encoder.start()
|
|
102
|
+
encoder.enter(asn1.Numbers.Sequence)
|
|
103
|
+
encoder.write(str(hardware_type_oid), asn1.Numbers.ObjectIdentifier)
|
|
104
|
+
encoder.write(str(hardware_serial_number), asn1.Numbers.OctetString)
|
|
105
|
+
encoder.leave()
|
|
106
|
+
hw_module_name = encoder.output()
|
|
107
|
+
|
|
108
|
+
sname = x509.Name("") # SubjectName should be blank
|
|
109
|
+
|
|
110
|
+
issuer_name = mica_cert.subject
|
|
111
|
+
iname = x509.Name(issuer_name)
|
|
112
|
+
cert = (
|
|
113
|
+
x509.CertificateBuilder()
|
|
114
|
+
.subject_name(sname)
|
|
115
|
+
.issuer_name(iname)
|
|
116
|
+
.public_key(csr.public_key())
|
|
117
|
+
.serial_number(x509.random_serial_number())
|
|
118
|
+
.not_valid_before(valid_from)
|
|
119
|
+
.not_valid_after(valid_to)
|
|
120
|
+
.add_extension(
|
|
121
|
+
x509.BasicConstraints(ca=False, path_length=None),
|
|
122
|
+
critical=True,
|
|
123
|
+
)
|
|
124
|
+
.add_extension(
|
|
125
|
+
x509.KeyUsage(
|
|
126
|
+
key_agreement=True,
|
|
127
|
+
key_cert_sign=False,
|
|
128
|
+
crl_sign=False,
|
|
129
|
+
digital_signature=True,
|
|
130
|
+
content_commitment=False,
|
|
131
|
+
key_encipherment=False,
|
|
132
|
+
data_encipherment=False,
|
|
133
|
+
encipher_only=False,
|
|
134
|
+
decipher_only=False,
|
|
135
|
+
),
|
|
136
|
+
critical=True,
|
|
137
|
+
)
|
|
138
|
+
.add_extension(
|
|
139
|
+
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
|
|
140
|
+
mica_cert.extensions.get_extension_for_class(
|
|
141
|
+
x509.SubjectKeyIdentifier
|
|
142
|
+
).value
|
|
143
|
+
),
|
|
144
|
+
critical=False,
|
|
145
|
+
)
|
|
146
|
+
.add_extension(
|
|
147
|
+
x509.SubjectAlternativeName(
|
|
148
|
+
general_names=[
|
|
149
|
+
x509.OtherName(SEP2_HARDWARE_MODULE_NAME, hw_module_name)
|
|
150
|
+
]
|
|
151
|
+
),
|
|
152
|
+
critical=True,
|
|
153
|
+
)
|
|
154
|
+
.add_extension(
|
|
155
|
+
x509.CertificatePolicies(policies=policies),
|
|
156
|
+
critical=True,
|
|
157
|
+
)
|
|
158
|
+
.sign(mica_key, SHA256())
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
serca_pem_data = ""
|
|
162
|
+
if serca_cert_path:
|
|
163
|
+
with open(serca_cert_path, "rb") as pem_file:
|
|
164
|
+
serca_pem_data = pem_file.read()
|
|
165
|
+
|
|
166
|
+
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
|
167
|
+
with open(filename, "wb") as fh:
|
|
168
|
+
fh.write(cert_pem)
|
|
169
|
+
# Append the intermediate certificate
|
|
170
|
+
fh.write(mica_pem_data)
|
|
171
|
+
# Append the root certificate
|
|
172
|
+
if serca_pem_data:
|
|
173
|
+
fh.write(serca_pem_data)
|
|
174
|
+
log.info("Created Cert %s", filename)
|
|
175
|
+
return filename
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
from itertools import zip_longest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cryptography import x509
|
|
7
|
+
from cryptography.hazmat.primitives import serialization
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def group_hex(item: str, group_size: int = 4) -> str:
|
|
13
|
+
"""Group into groups of four"""
|
|
14
|
+
|
|
15
|
+
def grouper(iterable, n, fillvalue=None):
|
|
16
|
+
args = [iter(iterable)] * n
|
|
17
|
+
return zip_longest(*args, fillvalue=fillvalue)
|
|
18
|
+
|
|
19
|
+
return "-".join("".join(x) for x in grouper(item, n=group_size))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_lfdi(fingerprint: str, group: bool = True) -> str:
|
|
23
|
+
"""Long-form device identifier (LFDI)
|
|
24
|
+
The LFDI SHALL be the certificate fingerprint left-truncated to 160 bits.
|
|
25
|
+
For display purposes, this SHALL be expressed as 40 hexadecimal digits in
|
|
26
|
+
groups of four.
|
|
27
|
+
"""
|
|
28
|
+
lfdi = fingerprint.replace("-", "")[0:40].upper()
|
|
29
|
+
if not group:
|
|
30
|
+
return lfdi
|
|
31
|
+
return group_hex(lfdi)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_sfdi(fingerprint, group: bool = True) -> str:
|
|
35
|
+
"""Short-form device identifier (SFDI)
|
|
36
|
+
The SFDI SHALL be the certificate fingerprint left-truncated to 36 bits.
|
|
37
|
+
For display purposes, this SHALL be expressed as 11 decimal (base 10) digits,
|
|
38
|
+
with an additional sum-of-digits checksum digit right concatenated.
|
|
39
|
+
"""
|
|
40
|
+
trunc = fingerprint.replace("-", "")[0:9]
|
|
41
|
+
sfdi = int(trunc, base=16) # Convert from Base 16 to Base 10
|
|
42
|
+
digits = map(int, str(sfdi))
|
|
43
|
+
checksum = 10 - sum(digits) % 10
|
|
44
|
+
if checksum == 10:
|
|
45
|
+
checksum = 0
|
|
46
|
+
|
|
47
|
+
full_sfdi = str(sfdi) + str(checksum)
|
|
48
|
+
if not group:
|
|
49
|
+
return full_sfdi
|
|
50
|
+
return group_hex(full_sfdi, 3)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_fingerprint(cert_path: Path) -> str:
|
|
54
|
+
"""Certificate fingerprint
|
|
55
|
+
The certificate fingerprint is the result of performing a SHA256 operation
|
|
56
|
+
over the whole DER-encoded certificate and is used to derive the SFDI and LFDI.
|
|
57
|
+
"""
|
|
58
|
+
with open(cert_path, "rb") as f:
|
|
59
|
+
fbytes = f.read() # read entire file as bytes
|
|
60
|
+
readable_hash = hashlib.sha256(fbytes).hexdigest().upper()
|
|
61
|
+
return readable_hash
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_der_certificate_lfdi(cert_path: Path, group: bool = True):
|
|
65
|
+
"""Load X.509 DER Certificate and return LFDI"""
|
|
66
|
+
|
|
67
|
+
if not cert_path.exists():
|
|
68
|
+
raise ValueError(f"Cert not found at {cert_path}")
|
|
69
|
+
|
|
70
|
+
fingerprint = get_fingerprint(cert_path)
|
|
71
|
+
return get_lfdi(fingerprint, group=group)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_pem_certificate_lfdi(cert_path: Path, group: bool = True):
|
|
75
|
+
"""Load X.509 DER Certificate in PEM format and return LFDI"""
|
|
76
|
+
|
|
77
|
+
if not cert_path.exists():
|
|
78
|
+
raise ValueError(f"Cert not found at {cert_path}")
|
|
79
|
+
|
|
80
|
+
with open(cert_path, "rb") as pem_file:
|
|
81
|
+
cert_data = pem_file.read()
|
|
82
|
+
|
|
83
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
|
84
|
+
der_bytes = cert.public_bytes(serialization.Encoding.DER)
|
|
85
|
+
fingerprint = hashlib.sha256(der_bytes).hexdigest().upper()
|
|
86
|
+
lfdi = get_lfdi(fingerprint, group=group)
|
|
87
|
+
return lfdi
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
def get_role_flag(
|
|
2
|
+
is_mirror=0, is_premise=0, is_pev=0, is_der=0, is_revenue=0, is_dc=0, is_submeter=0
|
|
3
|
+
) -> tuple[str, str]:
|
|
4
|
+
"""
|
|
5
|
+
RoleFlagsType object (HexBinary16)
|
|
6
|
+
Specifies the roles that apply to a usage point.
|
|
7
|
+
Bit 0—isMirror—SHALL be set if the server is not the measurement device
|
|
8
|
+
Bit 1—isPremisesAggregationPoint—SHALL be set if the UsagePoint is the
|
|
9
|
+
point of delivery for a premises
|
|
10
|
+
Bit 2—isPEV—SHALL be set if the usage applies to an electric vehicle
|
|
11
|
+
Bit 3—isDER—SHALL be set if the usage applies to a distributed energy resource,
|
|
12
|
+
capable of delivering
|
|
13
|
+
power to the grid
|
|
14
|
+
Bit 4—isRevenueQuality—SHALL be set if usage was measured by a device certified
|
|
15
|
+
as revenue quality
|
|
16
|
+
Bit 5—isDC—SHALL be set if the usage point measures direct current
|
|
17
|
+
Bit 6—isSubmeter—SHALL be set if the usage point is not a premises aggregation point
|
|
18
|
+
Bit 7 to 15—Reserved
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
a = is_mirror
|
|
22
|
+
b = is_premise
|
|
23
|
+
c = is_pev
|
|
24
|
+
d = is_der
|
|
25
|
+
e = is_revenue
|
|
26
|
+
f = is_dc
|
|
27
|
+
g = is_submeter
|
|
28
|
+
|
|
29
|
+
binary_str = f"000000000{g}{f}{e}{d}{c}{b}{a}"
|
|
30
|
+
val = int(binary_str, 2)
|
|
31
|
+
hex_str = str(hex(val))[2:].zfill(2).upper()
|
|
32
|
+
return binary_str, hex_str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_quality_flag(
|
|
36
|
+
valid=0, manual=0, estimate=0, interpolated=0, questionable=0, derived=0, future=0
|
|
37
|
+
) -> tuple[str, str]:
|
|
38
|
+
"""
|
|
39
|
+
qualityFlags (HexBinary16)
|
|
40
|
+
Bit 0 - valid: data that has gone through all required validation
|
|
41
|
+
checks and either passed them all or has been verified
|
|
42
|
+
Bit 1 - manually edited: Replaced or approved by a human
|
|
43
|
+
Bit 2 - estimated using reference day: data value was replaced
|
|
44
|
+
by a machine computed value based on analysis of historical
|
|
45
|
+
data using the same type of measurement.
|
|
46
|
+
Bit 3 - estimated using linear interpolation: data value was
|
|
47
|
+
computed using linear interpolation based on the readings
|
|
48
|
+
before and after it
|
|
49
|
+
Bit 4 - questionable: data that has failed one or more checks
|
|
50
|
+
Bit 5 - derived: data that has been calculated (using logic or
|
|
51
|
+
mathematical operations), not necessarily measured directly
|
|
52
|
+
Bit 6 - projected (forecast): data that has been calculated as a
|
|
53
|
+
projection or forecast of future reading
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
a = valid
|
|
57
|
+
b = manual
|
|
58
|
+
c = estimate
|
|
59
|
+
d = interpolated
|
|
60
|
+
e = questionable
|
|
61
|
+
f = derived
|
|
62
|
+
g = future
|
|
63
|
+
|
|
64
|
+
binary_str = f"000000000{g}{f}{e}{d}{c}{b}{a}"
|
|
65
|
+
val = int(binary_str, 2)
|
|
66
|
+
hex_str = str(hex(val))[2:].zfill(2).upper()
|
|
67
|
+
return binary_str, hex_str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_connect_status(
|
|
71
|
+
connected=0, available=0, operating=0, test=0, fault=0
|
|
72
|
+
) -> tuple[str, str]:
|
|
73
|
+
"""
|
|
74
|
+
ConnectStatusType object (HexBinary8)
|
|
75
|
+
0 = Connected
|
|
76
|
+
1 = Available
|
|
77
|
+
2 = Operating
|
|
78
|
+
3 = Test
|
|
79
|
+
4 = Fault/Error
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
a = connected
|
|
83
|
+
b = available
|
|
84
|
+
c = operating
|
|
85
|
+
d = test
|
|
86
|
+
e = fault
|
|
87
|
+
|
|
88
|
+
binary_str = f"000{e}{d}{c}{b}{a}"
|
|
89
|
+
val = int(binary_str, 2)
|
|
90
|
+
hex_str = str(hex(val))[2:].zfill(2).upper()
|
|
91
|
+
return binary_str, hex_str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_modes_supported(modes: list[str]) -> tuple[str, str]:
|
|
95
|
+
"""DERControlType object (HexBinary32)
|
|
96
|
+
Control modes supported by the DER. Bit positions SHALL be defined as follows:
|
|
97
|
+
0 = Charge mode
|
|
98
|
+
1 = Discharge mode
|
|
99
|
+
2 = opModConnect (connect/disconnect—implies galvanic isolation)
|
|
100
|
+
3 = opModEnergize (energize/de-energize)
|
|
101
|
+
4 = opModFixedPFAbsorbW (fixed power factor setpoint when absorbing active power)
|
|
102
|
+
5 = opModFixedPFInjectW (fixed power factor setpoint when injecting active power)
|
|
103
|
+
6 = opModFixedVar (reactive power setpoint)
|
|
104
|
+
7 = opModFixedW (charge/discharge setpoint)
|
|
105
|
+
8 = opModFreqDroop (Frequency-Watt Parameterized mode)
|
|
106
|
+
9 = opModFreqWatt (Frequency-Watt Curve mode)
|
|
107
|
+
10 = opModHFRTMayTrip (High Frequency Ride-Through, May Trip mode)
|
|
108
|
+
11 = opModHFRTMustTrip (High Frequency Ride-Through, Must Trip mode)
|
|
109
|
+
12 = opModHVRTMayTrip (High Voltage Ride-Through, May Trip mode)
|
|
110
|
+
13 = opModHVRTMomentaryCessation (HV Ride-Through, Momentary Cessation mode)
|
|
111
|
+
14 = opModHVRTMustTrip (High Voltage Ride-Through, Must Trip mode)
|
|
112
|
+
15 = opModLFRTMayTrip (Low Frequency Ride-Through, May Trip mode)
|
|
113
|
+
16 = opModLFRTMustTrip (Low Frequency Ride-Through, Must Trip mode)
|
|
114
|
+
17 = opModLVRTMayTrip (Low Voltage Ride-Through, May Trip mode)
|
|
115
|
+
18 = opModLVRTMomentaryCessation (LV Ride-Through, Momentary Cessation mode)
|
|
116
|
+
19 = opModLVRTMustTrip (Low Voltage Ride-Through, Must Trip mode)
|
|
117
|
+
20 = opModMaxLimW (maximum active power)
|
|
118
|
+
21 = opModTargetVar (target reactive power)
|
|
119
|
+
22 = opModTargetW (target active power)
|
|
120
|
+
23 = opModVoltVar (Volt-Var mode)
|
|
121
|
+
24 = opModVoltWatt (Volt-Watt mode)
|
|
122
|
+
25 = opModWattPF (Watt-Powerfactor mode)
|
|
123
|
+
26 = opModWattVar (Watt-Var mode)
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
a = 1 if "Charge mode" in modes else 0
|
|
127
|
+
b = 1 if "Discharge mode" in modes else 0
|
|
128
|
+
c = 1 if "opModConnect" in modes else 0
|
|
129
|
+
d = 1 if "opModEnergize" in modes else 0
|
|
130
|
+
e = 1 if "opModFixedPFAbsorbW" in modes else 0
|
|
131
|
+
f = 1 if "opModFixedPFInjectW" in modes else 0
|
|
132
|
+
g = 1 if "opModFixedVar" in modes else 0
|
|
133
|
+
h = 1 if "opModFixedW" in modes else 0
|
|
134
|
+
i = 1 if "opModFreqDroop" in modes else 0
|
|
135
|
+
j = 1 if "opModFreqWatt" in modes else 0
|
|
136
|
+
k = 1 if "opModHFRTMayTrip" in modes else 0
|
|
137
|
+
l = 1 if "opModHFRTMustTrip" in modes else 0 # noqa: E741
|
|
138
|
+
m = 1 if "opModHVRTMayTrip" in modes else 0
|
|
139
|
+
n = 1 if "opModHVRTMomentaryCessation" in modes else 0
|
|
140
|
+
o = 1 if "opModHVRTMustTrip" in modes else 0
|
|
141
|
+
p = 1 if "opModLFRTMayTrip" in modes else 0
|
|
142
|
+
q = 1 if "opModLFRTMustTrip" in modes else 0
|
|
143
|
+
r = 1 if "opModLVRTMayTrip" in modes else 0
|
|
144
|
+
s = 1 if "opModLVRTMomentaryCessation" in modes else 0
|
|
145
|
+
t = 1 if "opModLVRTMustTrip" in modes else 0
|
|
146
|
+
u = 1 if "opModMaxLimW" in modes else 0
|
|
147
|
+
v = 1 if "opModTargetVar" in modes else 0
|
|
148
|
+
w = 1 if "opModTargetW" in modes else 0
|
|
149
|
+
x = 1 if "opModVoltVar" in modes else 0
|
|
150
|
+
y = 1 if "opModVoltWatt" in modes else 0
|
|
151
|
+
z = 1 if "opModWattPF" in modes else 0
|
|
152
|
+
aa = 1 if "opModWattVar" in modes else 0
|
|
153
|
+
|
|
154
|
+
binary_str = f"00000{aa}{z}{y}{x}{w}{v}{u}{t}{s}{r}{q}"
|
|
155
|
+
binary_str += f"{p}{o}{n}{m}{l}{k}{j}{i}{h}{g}{f}{e}{d}{c}{b}{a}"
|
|
156
|
+
val = int(binary_str, 2)
|
|
157
|
+
hex_str = str(hex(val))[2:].zfill(8).upper()
|
|
158
|
+
return binary_str, hex_str
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_doe_modes_supported(modes: list[str]) -> tuple[str, str]:
|
|
162
|
+
"""DERControlType object (HexBinary32)
|
|
163
|
+
Control modes supported by the DER. Bit positions SHALL be defined as follows:
|
|
164
|
+
0 = opModExpLimW
|
|
165
|
+
1 = opModImpLimW
|
|
166
|
+
2 = opModGenLimW
|
|
167
|
+
3 = opModLoadLimW
|
|
168
|
+
"""
|
|
169
|
+
a = 1 if "opModExpLimW" in modes else 0
|
|
170
|
+
b = 1 if "opModImpLimW" in modes else 0
|
|
171
|
+
c = 1 if "opModGenLimW" in modes else 0
|
|
172
|
+
d = 1 if "opModLoadLimW" in modes else 0
|
|
173
|
+
|
|
174
|
+
binary_str = f"000000000000{d}{c}{b}{a}"
|
|
175
|
+
val = int(binary_str, 2)
|
|
176
|
+
hex_str = str(hex(val))[2:].zfill(8).upper()
|
|
177
|
+
return binary_str, hex_str
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
from itertools import zip_longest
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
log = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
# When generatating IDs, the PEN should always be used at the end
|
|
9
|
+
# to ensure that there are no conflicts between entities
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def group_hex(item: str, group_size: int = 4) -> str:
|
|
13
|
+
"""Group into groups of four"""
|
|
14
|
+
|
|
15
|
+
def grouper(iterable, n, fillvalue=None):
|
|
16
|
+
args = [iter(iterable)] * n
|
|
17
|
+
return zip_longest(*args, fillvalue=fillvalue)
|
|
18
|
+
|
|
19
|
+
return "-".join("".join(x) for x in grouper(item, n=group_size))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_mrid(pen: int, group: bool = True) -> str:
|
|
23
|
+
"""Generate an random mRID"""
|
|
24
|
+
random_hex = uuid4().hex
|
|
25
|
+
mrid = f"{random_hex[0:24]}{pen:08}".upper()
|
|
26
|
+
if not group:
|
|
27
|
+
return mrid
|
|
28
|
+
return group_hex(mrid)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def proxy_device_lfdi(pen: int, con_id: str, group: bool = True) -> str:
|
|
32
|
+
"""Generate a LFDI for use by a cloud proxy"""
|
|
33
|
+
id_hash = hashlib.sha256(con_id.encode("utf-8")).hexdigest().upper()
|
|
34
|
+
lfdi = f"{id_hash[0:32]}{pen:08}"
|
|
35
|
+
if not group:
|
|
36
|
+
return lfdi
|
|
37
|
+
return group_hex(lfdi)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from sep2tools.cert_create import generate_key_and_csr
|
|
4
|
+
|
|
5
|
+
CERT_DIR = Path("certs")
|
|
6
|
+
CERT_DIR.mkdir(exist_ok=True)
|
|
7
|
+
EXAMPLE_PEN = 1234
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_key_creation():
|
|
11
|
+
"""Create Private Key and CSR"""
|
|
12
|
+
|
|
13
|
+
key, csr = generate_key_and_csr(CERT_DIR / "key.pem", CERT_DIR / "csr.pem")
|
|
14
|
+
|
|
15
|
+
with open(key) as fh:
|
|
16
|
+
lines = fh.readlines()
|
|
17
|
+
assert lines[0] == "-----BEGIN PRIVATE KEY-----\n"
|
|
18
|
+
|
|
19
|
+
with open(csr) as fh:
|
|
20
|
+
lines = fh.readlines()
|
|
21
|
+
assert lines[0] == "-----BEGIN CERTIFICATE REQUEST-----\n"
|
|
22
|
+
|
|
23
|
+
key.unlink() # Delete to cleanup
|
|
24
|
+
csr.unlink() # Delete to cleanup
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from sep2tools import get_lfdi, get_sfdi
|
|
2
|
+
|
|
3
|
+
EXAMPLE_FINGERPRINT = (
|
|
4
|
+
"3E4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5-349E-2AD7-4567-2ED1-45EE-213A"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_lfdi():
|
|
10
|
+
"""Test the LFDI calculation"""
|
|
11
|
+
lfdi = get_lfdi(EXAMPLE_FINGERPRINT)
|
|
12
|
+
assert lfdi == "3E4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5"
|
|
13
|
+
|
|
14
|
+
lfdi = get_lfdi(EXAMPLE_FINGERPRINT, group=False)
|
|
15
|
+
assert lfdi == "3E4F45AB31EDFE5B67E343E5E4562E31984E23E5"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_sfdi():
|
|
19
|
+
"""Test the SFDI calculation"""
|
|
20
|
+
|
|
21
|
+
sfdi = get_sfdi(EXAMPLE_FINGERPRINT, group=False)
|
|
22
|
+
assert sfdi == "167261211391"
|
|
23
|
+
|
|
24
|
+
sfdi = get_sfdi(EXAMPLE_FINGERPRINT)
|
|
25
|
+
assert sfdi == "167-261-211-391"
|
|
26
|
+
|
|
27
|
+
# Ensure checksum is valid
|
|
28
|
+
digits = list(map(int, sfdi.replace("-", "")))
|
|
29
|
+
assert sum(digits) % 10 == 0
|
|
30
|
+
|
|
31
|
+
# Should also work if you pass the LFDI
|
|
32
|
+
lfdi = get_lfdi(EXAMPLE_FINGERPRINT)
|
|
33
|
+
sfdi = get_sfdi(lfdi)
|
|
34
|
+
assert sfdi == "167-261-211-391"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from sep2tools.cert_create import convert_pem_to_der
|
|
4
|
+
from sep2tools.cert_id import get_der_certificate_lfdi, get_pem_certificate_lfdi
|
|
5
|
+
|
|
6
|
+
EXAMPLE_CERT = Path("certs/example_cert.pem")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_example_cert_lfdi():
|
|
10
|
+
"""Test the LFDI calculation"""
|
|
11
|
+
|
|
12
|
+
lfdi1 = get_pem_certificate_lfdi(EXAMPLE_CERT)
|
|
13
|
+
der = convert_pem_to_der(EXAMPLE_CERT)
|
|
14
|
+
lfdi2 = get_der_certificate_lfdi(der)
|
|
15
|
+
assert lfdi1 == lfdi2
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from sep2tools.hexmaps import (
|
|
2
|
+
get_connect_status,
|
|
3
|
+
get_doe_modes_supported,
|
|
4
|
+
get_modes_supported,
|
|
5
|
+
get_quality_flag,
|
|
6
|
+
get_role_flag,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_role_flags():
|
|
11
|
+
"""Test the role flag mappings"""
|
|
12
|
+
|
|
13
|
+
# Site Reading
|
|
14
|
+
binval, hexval = get_role_flag(is_mirror=1, is_premise=1)
|
|
15
|
+
assert hexval == "03"
|
|
16
|
+
|
|
17
|
+
# DER Reading
|
|
18
|
+
binval, hexval = get_role_flag(is_mirror=1, is_der=1, is_submeter=1)
|
|
19
|
+
assert hexval == "49"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_quality_flags():
|
|
23
|
+
"""Test the quality mappings"""
|
|
24
|
+
|
|
25
|
+
# Valid
|
|
26
|
+
binval, hexval = get_quality_flag(valid=1)
|
|
27
|
+
assert hexval == "01"
|
|
28
|
+
|
|
29
|
+
# Questionable
|
|
30
|
+
binval, hexval = get_quality_flag(questionable=1)
|
|
31
|
+
assert hexval == "10"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_connect_status():
|
|
35
|
+
"""Test the connect status mappings"""
|
|
36
|
+
|
|
37
|
+
# Connected
|
|
38
|
+
binval, hexval = get_connect_status(connected=1, available=1, operating=1)
|
|
39
|
+
assert hexval == "07"
|
|
40
|
+
|
|
41
|
+
# Offline
|
|
42
|
+
binval, hexval = get_connect_status(connected=0, available=0, operating=0)
|
|
43
|
+
assert hexval == "00"
|
|
44
|
+
|
|
45
|
+
# Fault
|
|
46
|
+
binval, hexval = get_connect_status(connected=1, fault=1)
|
|
47
|
+
assert hexval == "11"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_modes_supported():
|
|
51
|
+
"""Test the modes supported mappings"""
|
|
52
|
+
|
|
53
|
+
modes = ["opModMaxLimW"]
|
|
54
|
+
binval, hexval = get_modes_supported(modes=modes)
|
|
55
|
+
assert hexval == "00100000"
|
|
56
|
+
|
|
57
|
+
modes = ["opModEnergize", "opConnect"]
|
|
58
|
+
binval, hexval = get_modes_supported(modes=modes)
|
|
59
|
+
assert hexval == "00000008"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_doe_modes_supported():
|
|
63
|
+
"""Test the modes supported mappings"""
|
|
64
|
+
|
|
65
|
+
modes = ["opModExpLimW"]
|
|
66
|
+
binval, hexval = get_doe_modes_supported(modes=modes)
|
|
67
|
+
assert hexval == "00000001"
|
|
68
|
+
|
|
69
|
+
modes = ["opModExpLimW", "opModImpLimW", "opModGenLimW", "opModLoadLimW"]
|
|
70
|
+
binval, hexval = get_doe_modes_supported(modes=modes)
|
|
71
|
+
assert hexval == "0000000F"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from sep2tools.ids import generate_mrid, proxy_device_lfdi
|
|
2
|
+
|
|
3
|
+
EXAMPLE_PEN = 1234
|
|
4
|
+
EXAMPLE_CPID = "NMI0001234"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_proxy_lfdi():
|
|
8
|
+
"""Test the LFDI used by a proxy"""
|
|
9
|
+
lfdi = proxy_device_lfdi(EXAMPLE_PEN, EXAMPLE_CPID)
|
|
10
|
+
assert lfdi == "B538-D994-2C7B-5B83-1AED-81A1-FEC4-6B3D-0000-1234"
|
|
11
|
+
|
|
12
|
+
lfdi = proxy_device_lfdi(EXAMPLE_PEN, EXAMPLE_CPID, group=False)
|
|
13
|
+
assert lfdi == "B538D9942C7B5B831AED81A1FEC46B3D00001234"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_mrid_generation():
|
|
17
|
+
"""Test the LFDI used by a proxy"""
|
|
18
|
+
|
|
19
|
+
mrid1 = generate_mrid(EXAMPLE_PEN)
|
|
20
|
+
assert mrid1[-9:] == "0000-1234"
|
|
21
|
+
mrid2 = generate_mrid(EXAMPLE_PEN)
|
|
22
|
+
assert mrid2[-9:] == "0000-1234"
|
|
23
|
+
|
|
24
|
+
assert mrid1 != mrid2
|
|
25
|
+
|
|
26
|
+
mrid3 = generate_mrid(EXAMPLE_PEN, group=False)
|
|
27
|
+
assert mrid3[-8:] == "00001234"
|