diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..16b70af --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +PYTHON = python3 +SRC_FILES = $(wildcard src/**/*.py) + +build: dist + +dist: $(SRC_FILES) + $(PYTHON) -m pip install --upgrade build + $(PYTHON) -m build + +.PHONY: test +test: + $(PYTHON) -m unittest discover -v + +.PHONY: clean +clean: + rm -rf dist + +.PHONY: install +install: # only there for testing purposes + pip install -r requirements.txt + +.PHONY: run-example +run-example: + $(PYTHON) ./tests/example \ No newline at end of file diff --git a/README.md b/README.md index 0500c4e..a7d8a70 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,84 @@ # alvarium-sdk-python -Implementation of the Alvarium SDK using Python +Python implementation of the Project Alvarium SDK + +# SDK Interface + +The SDK provides a minimal API -- DefaultSdk(), Create(), Mutate(), Transit(), Publish() and Close(). + +### NewSdk() + +```python +def DefaultSdk(self, annotators: List[Annotator], config: SdkInfo, logger: Logger) --> DefaultSdk +``` + +Used to instantiate a new SDK instance with the specified list of annotators. + +Takes a list of annotators, a populated configuration and a logger instance. Returns an SDK instance. + +### Create() + +```python +def create(self, data: bytes, properties: PropertyBag = None) -> None +``` + +Used to register creation of new data with the SDK. Passes data through the SDK instance's list of annotators. + +SDK instance method. Parameters include: + + +- data -- The data being created represented as bytes + +- properties -- Provide a property bag that may be used by individual annotators +### Mutate() + +```python +def mutate(self, old_data: bytes, new_data: bytes, properties: PropertyBag = None) -> None +``` + +Used to register mutation of existing data with the SDK. Passes data through the SDK instance's list of annotators. + +SDK instance method. Parameters include: + +- old_data -- The source data item that is being modified, represented as bytes + +- new_data -- The new data item resulting from the change, represented as bytes + +- properties -- Provide a property bag that may be used by individual annotators + +Calling this method will link the old data to the new in a lineage. Specific annotations will be applied to the `new` data element. + +### Transit() + +```python +def transit(self, data: bytes, properties: PropertyBag = None) -> None +``` + +Used to annotate data that is neither originated or modified but simply handed from one application to another. + +SDK instance method. Parameters include: + +- data -- The data being handled represented as bytes + +- properties -- Provide a property bag that may be used by individual annotators + +### Publish() + +```python +def publish(self, data: bytes, properties: PropertyBag = None) -> None +``` + +Used to annotate data that is neither originated or modified but **before** being handed to another application. + +SDK instance method. Parameters include: + +- data -- The data being handled represented as bytes + +- properties -- Provide a property bag that may be used by individual annotators + +### Close() + +```python +def close(self) -> None +``` + +SDK instance method. Ensures clean shutdown of the SDK and associated resources. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5a3c46 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7127afa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +ulid-py==1.1.0 +paho-mqtt==1.6.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8124655 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,23 @@ +[metadata] +name = alvarium-sdk +version = 0.0.1 +author = Alvarium org +description = Python implementation of the Alvarium sdk +long_description = file: README.md +long_description_content_type = text/markdown +url = https://eos2git.cec.lab.emc.com/octo-dcf/alvarium-sdk-python +project_urls = + Bug Tracker = https://eos2git.cec.lab.emc.com/octo-dcf/alvarium-sdk-python/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: Apache License + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.6 + +[options.packages.find] +where = src \ No newline at end of file diff --git a/src/alvarium/__init__.py b/src/alvarium/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alvarium/annotators/__init__.py b/src/alvarium/annotators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alvarium/annotators/contracts.py b/src/alvarium/annotators/contracts.py new file mode 100644 index 0000000..4e9eb73 --- /dev/null +++ b/src/alvarium/annotators/contracts.py @@ -0,0 +1,25 @@ +import json + +from dataclasses import dataclass + +@dataclass +class Signable: + """A data class that holds the seed (data) and signature for this data""" + + seed: str + signature: str + + @staticmethod + def from_json(data: str): + signable_json = json.loads(data) + return Signable(seed=str(signable_json["seed"]), signature=str(signable_json["signature"])) + + def to_json(self) -> str: + signable_json = { + "seed": str(self.seed), + "signature": str(self.signature) + } + return json.dumps(signable_json) + + def __str__(self) -> str: + return self.to_json() \ No newline at end of file diff --git a/src/alvarium/annotators/exceptions.py b/src/alvarium/annotators/exceptions.py new file mode 100644 index 0000000..7f8299a --- /dev/null +++ b/src/alvarium/annotators/exceptions.py @@ -0,0 +1,2 @@ +class AnnotatorException(Exception): + """A general exception type to be used by the annotators""" diff --git a/src/alvarium/annotators/factories.py b/src/alvarium/annotators/factories.py new file mode 100644 index 0000000..7e522b8 --- /dev/null +++ b/src/alvarium/annotators/factories.py @@ -0,0 +1,28 @@ +from alvarium.contracts.config import SdkInfo +from .interfaces import Annotator +from alvarium.contracts.annotation import AnnotationType +from .exceptions import AnnotatorException +from .mock import MockAnnotator +from .tpm import TpmAnnotator +from .pki import PkiAnnotator +from .source import SourceAnnotator +from .tls import TlsAnnotator + +class AnnotatorFactory(): + """A factory that provides multiple implementations of the Annotator interface""" + + def get_annotator(self, kind: AnnotationType, sdk_info: SdkInfo) -> Annotator: + + if kind == AnnotationType.MOCK: + return MockAnnotator(hash=sdk_info.hash.type, signature=sdk_info.signature, kind=kind) + elif kind == AnnotationType.TPM: + return TpmAnnotator(hash=sdk_info.hash.type, sign_info=sdk_info.signature) + elif kind == AnnotationType.SOURCE: + return SourceAnnotator(hash=sdk_info.hash.type, sign_info=sdk_info.signature) + elif kind == AnnotationType.TLS: + return TlsAnnotator(hash=sdk_info.hash.type, signature=sdk_info.signature) + elif kind == AnnotationType.PKI: + return PkiAnnotator(hash=sdk_info.hash.type, sign_info=sdk_info.signature) + else: + raise AnnotatorException("Annotator type is not supported") + diff --git a/src/alvarium/annotators/interfaces.py b/src/alvarium/annotators/interfaces.py new file mode 100644 index 0000000..5113129 --- /dev/null +++ b/src/alvarium/annotators/interfaces.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + +from alvarium.contracts.annotation import Annotation +from alvarium.utils import PropertyBag + +class Annotator(ABC): + """A unit responsible for annontating raw data and producing an Annotation object""" + + @abstractmethod + def execute(self, data:bytes, ctx: PropertyBag = None) -> Annotation: + pass \ No newline at end of file diff --git a/src/alvarium/annotators/mock.py b/src/alvarium/annotators/mock.py new file mode 100644 index 0000000..9815e3c --- /dev/null +++ b/src/alvarium/annotators/mock.py @@ -0,0 +1,43 @@ +import socket + +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.hash.contracts import HashType +from alvarium.hash.factories import HashProviderFactory +from alvarium.hash.exceptions import HashException +from alvarium.sign.contracts import SignInfo +from alvarium.utils import PropertyBag +from .interfaces import Annotator +from .exceptions import AnnotatorException + +class MockAnnotator(Annotator): + """a mock annotator to be used in unit tests""" + + hash: HashType + signature: SignInfo + kind: AnnotationType + + def __init__(self, hash: HashType, signature: SignInfo, kind: AnnotationType): + self.hash = hash + self.signature = signature + self.kind = kind + + def execute(self, data: bytes, ctx: PropertyBag = None) -> Annotation: + hashFactory = HashProviderFactory() + + try: + key = hashFactory.get_provider(self.hash).derive(data) + host = socket.gethostname() + sig = self.signature.public.type.__str__() + + annotation = Annotation(key, self.hash, host, self.kind, sig, True) + return annotation + + except HashException as e: + raise AnnotatorException("failed to hash data", e) + + except socket.herror as e: + raise AnnotatorException("could not get hostname", e) + + + + diff --git a/src/alvarium/annotators/pki.py b/src/alvarium/annotators/pki.py new file mode 100644 index 0000000..dea329a --- /dev/null +++ b/src/alvarium/annotators/pki.py @@ -0,0 +1,48 @@ +import socket + +from alvarium.sign.exceptions import SignException +from alvarium.sign.factories import SignProviderFactory +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.hash.contracts import HashType +from alvarium.sign.contracts import KeyInfo, SignInfo +from alvarium.utils import PropertyBag +from .contracts import Signable +from .utils import derive_hash, sign_annotation +from .interfaces import Annotator +from .exceptions import AnnotatorException + +class PkiAnnotator(Annotator): + + def __init__(self, hash: HashType, sign_info: SignInfo) -> None: + self.hash = hash + self.sign_info = sign_info + self.kind = AnnotationType.PKI + + def _verify_signature(self, key: KeyInfo, signable: Signable) -> bool: + """ Responsible for verifying the signature, returns true if the verification passed + , false otherwise.""" + try: + sign_provider = SignProviderFactory().get_provider(sign_type=key.type) + except SignException as e: + raise AnnotatorException("cannot get sign provider.", e) + + with open(key.path, 'r') as file: + pub_key = file.read() + return sign_provider.verify(key=bytes.fromhex(pub_key), + content=bytes(signable.seed, 'utf-8'), + signed=bytes.fromhex(signable.signature)) + + + def execute(self, data: bytes, ctx: PropertyBag = None) -> Annotation: + key = derive_hash(hash=self.hash, data=data) + host: str = socket.gethostname() + + # create Signable object + signable = Signable.from_json(data.decode('utf-8')) + is_satisfied: bool = self._verify_signature(key=self.sign_info.public, signable=signable) + + annotation = Annotation(key=key, host=host, hash=self.hash, kind=self.kind, is_satisfied=is_satisfied) + + signature: str = sign_annotation(key_info=self.sign_info.private, annotation=annotation) + annotation.signature = signature + return annotation \ No newline at end of file diff --git a/src/alvarium/annotators/source.py b/src/alvarium/annotators/source.py new file mode 100644 index 0000000..8fba6c0 --- /dev/null +++ b/src/alvarium/annotators/source.py @@ -0,0 +1,27 @@ +import socket + +from alvarium.hash.contracts import HashType +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.sign.contracts import SignInfo +from alvarium.utils import PropertyBag +from .interfaces import Annotator +from .utils import derive_hash, sign_annotation + +class SourceAnnotator(Annotator): + """ A unit used to provide lineage from one version of data to another as a result of + change or transformation""" + + def __init__(self, hash: HashType, sign_info: SignInfo) -> None: + self.hash = hash + self.kind = AnnotationType.SOURCE + self.sign_info = sign_info + + def execute(self, data: bytes, ctx: PropertyBag = None) -> Annotation: + key: str = derive_hash(hash=self.hash, data=data) + host: str = socket.gethostname() + + annotation = Annotation(key=key, hash=self.hash, host=host, kind=self.kind, is_satisfied=True) + + signature: str = sign_annotation(key_info=self.sign_info.private, annotation=annotation) + annotation.signature = signature + return annotation diff --git a/src/alvarium/annotators/tls.py b/src/alvarium/annotators/tls.py new file mode 100644 index 0000000..44f0f41 --- /dev/null +++ b/src/alvarium/annotators/tls.py @@ -0,0 +1,49 @@ +import socket +import ssl + +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.hash.exceptions import HashException +from alvarium.hash.contracts import HashType +from alvarium.sign.contracts import SignInfo +from alvarium.utils import PropertyBag +from .utils import derive_hash, sign_annotation +from .interfaces import Annotator +from .exceptions import AnnotatorException + + +class TlsAnnotator(Annotator): + + hash: HashType + signature: SignInfo + kind: AnnotationType + + def __init__(self, hash: HashType, signature: SignInfo): + self.hash = hash + self.signature = signature + self.kind = AnnotationType.TLS + + def execute(self, ctx: PropertyBag, data: bytes) -> Annotation: + try: + key = derive_hash(self.hash, data) + is_satisfied = False + + context = ctx.get_property(str(AnnotationType.TLS)) + if context != None: + # If none is returned then there is no error in handshake so tls is satisfied + if type(context) == ssl.SSLSocket: + try: + if context.do_handshake(block=True) == None: + is_satisfied = True + except: + pass + + annotation = Annotation(key= key, hash= self.hash, host= socket.gethostname(), kind= self.kind, is_satisfied= is_satisfied) + annotation_signture = sign_annotation(self.signature.private, annotation) + annotation.signature = str(annotation_signture) + return annotation + + except HashException as e: + raise AnnotatorException("failed to hash data", e) + + except socket.herror as e: + raise AnnotatorException("could not get hostname", e) \ No newline at end of file diff --git a/src/alvarium/annotators/tpm.py b/src/alvarium/annotators/tpm.py new file mode 100644 index 0000000..a33e9d0 --- /dev/null +++ b/src/alvarium/annotators/tpm.py @@ -0,0 +1,42 @@ +import socket + +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.hash.contracts import HashType +from alvarium.sign.contracts import SignInfo +from alvarium.utils import PropertyBag +from os import path +from .interfaces import Annotator +from .utils import derive_hash, sign_annotation + +class TpmAnnotator(Annotator): + + _DIRECT_TPM_PATH = "/dev/tpm0" + _TPM_KERNEL_MANAGED_PATH = "/dev/tpmrm0" + + def __init__(self, hash: HashType, sign_info: SignInfo) -> None: + self.hash = hash + self.sign_info = sign_info + self.kind = AnnotationType.TPM + + def _check_tpm_exists(self, directory: str) -> bool: + if not path.exists(path=directory): + return False + + try: + file = open(directory, "w+") + file.close() + return True + except OSError as e: + return False + + def execute(self, data: bytes, ctx: PropertyBag = None) -> Annotation: + key: str = derive_hash(hash=self.hash, data=data) + host: str = socket.gethostname() + is_satisfied:bool = self._check_tpm_exists(self._TPM_KERNEL_MANAGED_PATH) or \ + self._check_tpm_exists(self._DIRECT_TPM_PATH) + + annotation = Annotation(key=key, host=host, hash=self.hash, kind=self.kind, is_satisfied=is_satisfied) + + signature: str = sign_annotation(key_info=self.sign_info.private, annotation=annotation) + annotation.signature = signature + return annotation \ No newline at end of file diff --git a/src/alvarium/annotators/utils.py b/src/alvarium/annotators/utils.py new file mode 100644 index 0000000..423e9de --- /dev/null +++ b/src/alvarium/annotators/utils.py @@ -0,0 +1,29 @@ +from alvarium.hash.contracts import HashType +from alvarium.hash.exceptions import HashException +from alvarium.hash.factories import HashProviderFactory +from alvarium.sign.contracts import KeyInfo +from alvarium.sign.factories import SignProviderFactory +from alvarium.sign.exceptions import SignException +from alvarium.contracts.annotation import Annotation +from .exceptions import AnnotatorException + + +def derive_hash(hash: HashType, data: bytes) -> str: + """A helper to ease the hashing of incoming data.""" + try: + hash_provider = HashProviderFactory().get_provider(hash_type=hash) + return hash_provider.derive(data=data) + except HashException as e: + raise AnnotatorException("cannot hash data.", e) + +def sign_annotation(key_info: KeyInfo, annotation: Annotation) -> str: + """A helper to ease the signing process of an annotation""" + try: + sign_provider = SignProviderFactory().get_provider(sign_type=key_info.type) + with open(key_info.path, "r") as file: + key = file.read() + return sign_provider.sign(key=bytes.fromhex(key), content=bytes(annotation.to_json(), 'utf-8')) + except SignException as e: + raise AnnotatorException("cannot sign annotation.", e) + except OSError as e: + raise AnnotatorException("cannot open key file.", e) diff --git a/src/alvarium/contracts/__init__.py b/src/alvarium/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alvarium/contracts/annotation.py b/src/alvarium/contracts/annotation.py new file mode 100644 index 0000000..39bb286 --- /dev/null +++ b/src/alvarium/contracts/annotation.py @@ -0,0 +1,74 @@ +import ulid +import json + +from typing import List +from dataclasses import dataclass, field +from enum import Enum +from alvarium.hash.contracts import HashType +from datetime import datetime, timezone + +class AnnotationType(Enum): + TPM = "tpm" + PKI = "pki" + TLS = "tls" + SOURCE = "src" + MOCK = "mock" + + def __str__(self) -> str: + return f'{self.value}' + +@dataclass +class Annotation: + """A data class that encapsulates all of the data related to a specific annotation. + this will be generated by the annotators.""" + + key: str + hash: HashType + host: str + kind: AnnotationType + timestamp: str = field(default_factory=lambda : datetime.now(timezone.utc).astimezone().isoformat(), init=True) + id: ulid.ULID = field(default_factory=ulid.new, init=True) + is_satisfied: bool = None + signature: str = None + + def to_json(self) -> str: + annotation_json = {"id": str(self.id), "key": str(self.key), "hash": str(self.hash), + "host": str(self.host), "kind": str(self.kind), + "timestamp": str(self.timestamp)} + + if self.signature != None: + annotation_json["signature"] = str(self.signature) + + if self.is_satisfied != None: + annotation_json["isSatisfied"] = self.is_satisfied + + return json.dumps(annotation_json) + + @staticmethod + def from_json(data: str): + annotation_json = json.loads(data) + return Annotation(id=ulid.from_str(annotation_json["id"]), key=annotation_json["key"], + hash=HashType(annotation_json["hash"]), host=annotation_json["host"], + kind=AnnotationType(annotation_json["kind"]), signature=annotation_json["signature"], + is_satisfied=bool(annotation_json["isSatisfied"]), timestamp=annotation_json["timestamp"]) + + def __str__(self) -> str: + return self.to_json() + + +@dataclass +class AnnotationList: + items: List[Annotation] + + def to_json(self) -> str: + annotation_list_json = {"items": [json.loads(str(item)) for item in self.items]} + return json.dumps(annotation_list_json) + + @staticmethod + def from_json(data: str): + annotation_list_json = json.loads(data) + return AnnotationList(items=[Annotation.from_json(json.dumps(item)) for item in annotation_list_json["items"]]) + + def __str__(self) -> str: + return self.to_json() + diff --git a/src/alvarium/contracts/config.py b/src/alvarium/contracts/config.py new file mode 100644 index 0000000..dd7bb9e --- /dev/null +++ b/src/alvarium/contracts/config.py @@ -0,0 +1,37 @@ +import json + +from dataclasses import dataclass +from typing import List +from alvarium.hash.contracts import HashInfo +from alvarium.sign.contracts import SignInfo +from alvarium.streams.contracts import StreamInfo +from .annotation import AnnotationType + +@dataclass +class SdkInfo: + """A data class that encapsulates all of the config related to the SDK""" + + annotators: List[AnnotationType] + hash: HashInfo + signature: SignInfo + stream: StreamInfo + + @staticmethod + def from_json(data: str): + info_json = json.loads(data) + annotators = [AnnotationType(x) for x in info_json["annotators"]] + hash = HashInfo.from_json(json.dumps(info_json["hash"])) + signature = SignInfo.from_json(json.dumps(info_json["signature"])) + stream = StreamInfo.from_json(json.dumps(info_json["stream"])) + return SdkInfo(annotators=annotators, hash=hash, signature=signature, stream=stream) + + def to_json(self) -> str: + info_json = {} + info_json["annotators"] = [str(x) for x in self.annotators] + info_json["hash"] = json.loads(self.hash.to_json()) + info_json["signature"] = json.loads(self.signature.to_json()) + info_json["stream"] = json.loads(self.stream.to_json()) + return json.dumps(info_json) + + def __str__(self) -> str: + return self.to_json() \ No newline at end of file diff --git a/src/alvarium/contracts/publish.py b/src/alvarium/contracts/publish.py new file mode 100644 index 0000000..e10a9a3 --- /dev/null +++ b/src/alvarium/contracts/publish.py @@ -0,0 +1,34 @@ +import json +import base64 + +from enum import Enum +from dataclasses import dataclass +from typing import Any + +class SdkAction(Enum): + + CREATE = "create" + MUTATE = "mutate" + TRANSIT = "transit" + PUBLISH = "publish" + + def __str__(self) -> str: + return f'{self.value}' + +@dataclass +class PublishWrapper: + """A data class that encapsulates the data being published by stream providers""" + + action: SdkAction + message_type: str + content: Any + + def to_json(self) -> str: + # content is first encoded to base64 to match the go sdk implementation + content = base64.b64encode(bytes(str(self.content), 'utf-8')).decode('utf-8') + wrapper_json = {"action": str(self.action), "messageType": str(self.message_type), + "content": str(content)} + return json.dumps(wrapper_json) + + def __str__(self) -> str: + return self.to_json() \ No newline at end of file diff --git a/src/alvarium/default.py b/src/alvarium/default.py new file mode 100644 index 0000000..ce121de --- /dev/null +++ b/src/alvarium/default.py @@ -0,0 +1,64 @@ +from logging import Logger +from typing import List +from .sdk import Sdk +from .utils import PropertyBag +from .streams.factories import StreamProviderFactory +from .annotators.interfaces import Annotator +from .annotators.factories import AnnotatorFactory +from .contracts.config import SdkInfo +from .contracts.annotation import AnnotationList, AnnotationType +from .contracts.publish import PublishWrapper, SdkAction + +class DefaultSdk(Sdk): + """default implementation of the sdk interface""" + + def __init__(self, annotators: List[Annotator], config: SdkInfo, logger: Logger) -> None: + self.annotators = annotators + self.config = config + + self.stream = StreamProviderFactory().get_provider(self.config.stream) + self.stream.connect() + + self.logger = logger + self.logger.debug("stream provider connected successfully") + + def create(self, data: bytes, properties: PropertyBag = None) -> None: + annotation_list = AnnotationList(items=[ann.execute(data=data, ctx=properties) for ann in self.annotators]) + wrapper = PublishWrapper(action=SdkAction.CREATE, message_type=type(annotation_list).__name__, content=annotation_list) + + self.stream.publish(wrapper=wrapper) + self.logger.debug("data annotated and published successfully.") + + + def mutate(self, old_data: bytes, new_data: bytes, properties: PropertyBag = None) -> None: + source_annotator = AnnotatorFactory().get_annotator(kind=AnnotationType.SOURCE, sdk_info=self.config) + # TLS is ignored in mutate to prevent needless penalization + # See https://github.com/project-alvarium/alvarium-sdk-go/issues/19 + annotations = [ + source_annotator.execute(data=old_data), + *[ann.execute(data=new_data, ctx=properties) for ann in self.annotators if ann.kind != AnnotationType.TLS] + ] + annotation_list = AnnotationList(items=annotations) + wrapper = PublishWrapper(action=SdkAction.MUTATE, message_type=type(annotation_list).__name__, content=annotation_list) + + self.stream.publish(wrapper=wrapper) + self.logger.debug("data annotated and published successfully.") + + def transit(self, data: bytes, properties: PropertyBag = None) -> None: + annotaion_list = AnnotationList(items=[ann.execute(data=data, ctx = properties) for ann in self.annotators]) + wrapper = PublishWrapper(action=SdkAction.TRANSIT, message_type=type(annotaion_list).__name__, content=annotaion_list) + + self.stream.publish(wrapper=wrapper) + self.logger.debug("data annotated and published successfully.") + + def publish(self, data: bytes, properties: PropertyBag = None) -> None: + annotation_list = AnnotationList(items=[ann.execute(data=data, ctx = properties) for ann in self.annotators]) + wrapper = PublishWrapper(action=SdkAction.PUBLISH, message_type=type(annotation_list).__name__, content=annotation_list) + + self.stream.publish(wrapper=wrapper) + self.logger.debug("data annotated and published successfully.") + + def close(self) -> None: + self.stream.close() + self.logger.debug("sdk disposed") + diff --git a/src/alvarium/hash/__init__.py b/src/alvarium/hash/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alvarium/hash/contracts.py b/src/alvarium/hash/contracts.py new file mode 100644 index 0000000..36f6d4a --- /dev/null +++ b/src/alvarium/hash/contracts.py @@ -0,0 +1,30 @@ +from enum import Enum +from dataclasses import dataclass +import json + +class HashType(Enum): + + NONE = "none" + MD5 = "md5" + SHA256 = "sha256" + + def __str__(self) -> str: + return f'{self.value}' + +@dataclass +class HashInfo(): + """A data class that encapsulates the config related to hash opertations.""" + + type: HashType + + def to_json(self) -> str: + info_json = {"type": str(self.type)} + return json.dumps(info_json) + + @staticmethod + def from_json(data: str): + info_json = json.loads(data) + return HashInfo(type=HashType(info_json["type"])) + + def __str__(self) -> str: + return self.to_json() diff --git a/src/alvarium/hash/exceptions.py b/src/alvarium/hash/exceptions.py new file mode 100644 index 0000000..6809d32 --- /dev/null +++ b/src/alvarium/hash/exceptions.py @@ -0,0 +1,2 @@ +class HashException(Exception): + """a custom exception for hash related opertations""" \ No newline at end of file diff --git a/src/alvarium/hash/factories.py b/src/alvarium/hash/factories.py new file mode 100644 index 0000000..646e708 --- /dev/null +++ b/src/alvarium/hash/factories.py @@ -0,0 +1,20 @@ +from .sha256 import SHA256Provider +from .md5 import MD5Provider +from .exceptions import HashException +from .mock import NoneHashProvider +from .contracts import HashType +from .interfaces import HashProvider + +class HashProviderFactory: + """A factory that provides a way to instaniate different types of + Hash Providers.""" + + def get_provider(self, hash_type: HashType) -> HashProvider: + if hash_type == HashType.NONE: + return NoneHashProvider() + elif hash_type == HashType.SHA256: + return SHA256Provider() + elif hash_type == HashType.MD5: + return MD5Provider() + else: + raise HashException(f'{hash_type} is not implemented') \ No newline at end of file diff --git a/src/alvarium/hash/interfaces.py b/src/alvarium/hash/interfaces.py new file mode 100644 index 0000000..e621b9e --- /dev/null +++ b/src/alvarium/hash/interfaces.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +class HashProvider(ABC): + """a unit that provides an interface for hashing the incoming data""" + + @abstractmethod + def derive(self, data: bytes) -> str: + """returns the hash hex string representation of the input data""" + pass \ No newline at end of file diff --git a/src/alvarium/hash/md5.py b/src/alvarium/hash/md5.py new file mode 100644 index 0000000..b2f7c87 --- /dev/null +++ b/src/alvarium/hash/md5.py @@ -0,0 +1,10 @@ +from .interfaces import HashProvider +import hashlib + +class MD5Provider(HashProvider): + + def derive(self, data: bytes) -> str: + """returns the hexadecimal md5 hash representation of the given data""" + _m = hashlib.md5() + _m.update(data) + return _m.hexdigest() \ No newline at end of file diff --git a/src/alvarium/hash/mock.py b/src/alvarium/hash/mock.py new file mode 100644 index 0000000..ce2be5c --- /dev/null +++ b/src/alvarium/hash/mock.py @@ -0,0 +1,7 @@ +from .interfaces import HashProvider + +class NoneHashProvider(HashProvider): + """A mock implementation for the HashProvider interface""" + + def derive(self, data: bytes) -> str: + return data.decode("utf-8") \ No newline at end of file diff --git a/src/alvarium/hash/sha256.py b/src/alvarium/hash/sha256.py new file mode 100644 index 0000000..3a9cea7 --- /dev/null +++ b/src/alvarium/hash/sha256.py @@ -0,0 +1,10 @@ +from .interfaces import HashProvider +import hashlib + +class SHA256Provider(HashProvider): + + def derive(self, data: bytes) -> str: + """returns the hexadecimal md5 hash representation of the given data""" + _m = hashlib.sha256() + _m.update(data) + return _m.hexdigest() \ No newline at end of file diff --git a/src/alvarium/sdk.py b/src/alvarium/sdk.py new file mode 100644 index 0000000..22c82e0 --- /dev/null +++ b/src/alvarium/sdk.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +from alvarium.utils import PropertyBag + +class Sdk(ABC): + """This unit serves as an interface for the sdk.""" + + @abstractmethod + def create(self, data: bytes, properties: PropertyBag = None) -> None: + pass + + @abstractmethod + def mutate(self, old_data: bytes, new_data: bytes, properties: PropertyBag = None) -> None: + pass + + @abstractmethod + def transit(self, data: bytes, properties: PropertyBag = None) -> None: + pass + + @abstractmethod + def publish(self, data: bytes, properties: PropertyBag = None) -> None: + pass + + @abstractmethod + def close(self) -> None: + pass \ No newline at end of file diff --git a/src/alvarium/sign/__init__.py b/src/alvarium/sign/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alvarium/sign/contracts.py b/src/alvarium/sign/contracts.py new file mode 100644 index 0000000..019c314 --- /dev/null +++ b/src/alvarium/sign/contracts.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass +from enum import Enum +import json + +class SignType(Enum): + + ED25519 = "ed25519" + NONE = "none" + + def __str__(self) -> str: + return f'{self.value}' + +@dataclass +class KeyInfo: + """A data class that encapsulates the config related to key info""" + type: SignType + path: str + + def to_json(self) -> str: + info_json = {"type" : str(self.type), "path": self.path} + return json.dumps(info_json) + + @staticmethod + def from_json(data: str): + info_json = json.loads(data) + return KeyInfo(type = SignType(info_json["type"]), path=info_json["path"]) + + + def __str__(self) -> str: + return self.to_json() + +@dataclass +class SignInfo: + """A data class that encapsulates the config related to sign opertations.""" + public: KeyInfo + private: KeyInfo + + def to_json(self) -> str: + info_json = {} + info_json["public"] = json.loads(self.public.to_json()) + info_json["private"] = json.loads(self.private.to_json()) + return json.dumps(info_json) + + @staticmethod + def from_json(data: str): + info_json = json.loads(data) + return SignInfo(public = KeyInfo.from_json(json.dumps(info_json["public"])), + private = KeyInfo.from_json(json.dumps(info_json["private"]))) + + def __str__(self) -> str: + return self.to_json() \ No newline at end of file diff --git a/src/alvarium/sign/ed25519.py b/src/alvarium/sign/ed25519.py new file mode 100644 index 0000000..73f7263 --- /dev/null +++ b/src/alvarium/sign/ed25519.py @@ -0,0 +1,22 @@ +from .interfaces import SignProvider + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric import ed25519 + + +class Ed25519ignProvider(SignProvider): + """The implementation of the Ed25519 sign provider interface""" + + def sign(self, key: bytes, content: bytes) -> str: + key = key[:32] + loaded_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(key) + return bytes.hex(loaded_private_key.sign(content)) + + def verify(self, key: bytes, content: bytes, signed: bytes) -> bool: + loaded_public_key = ed25519.Ed25519PublicKey.from_public_bytes(key) + try: + loaded_public_key.verify(signed, content) + except InvalidSignature: + return False + + return True diff --git a/src/alvarium/sign/exceptions.py b/src/alvarium/sign/exceptions.py new file mode 100644 index 0000000..97a2e1f --- /dev/null +++ b/src/alvarium/sign/exceptions.py @@ -0,0 +1,2 @@ +class SignException(Exception): + """Custom exception for sign related operators""" \ No newline at end of file diff --git a/src/alvarium/sign/factories.py b/src/alvarium/sign/factories.py new file mode 100644 index 0000000..e90ada0 --- /dev/null +++ b/src/alvarium/sign/factories.py @@ -0,0 +1,20 @@ +from .interfaces import SignProvider +from .contracts import SignType +from .exceptions import SignException +from .mock import NoneSignProvider +from .ed25519 import Ed25519ignProvider + + +class SignProviderFactory: + """A factory that provides a way to instaniate different types of + Sign Providers.""" + + def get_provider(self, sign_type: SignType) -> SignProvider: + """A function returns sign provider based on what sign type it gets""" + + if sign_type == SignType.NONE: + return NoneSignProvider() + if sign_type == SignType.ED25519: + return Ed25519ignProvider() + else: + raise SignException(f'{sign_type} is not implemented yet') diff --git a/src/alvarium/sign/interfaces.py b/src/alvarium/sign/interfaces.py new file mode 100644 index 0000000..4d73388 --- /dev/null +++ b/src/alvarium/sign/interfaces.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + +class SignProvider(ABC): + """a unit that provides an interface for signing and verifying the data""" + + @abstractmethod + def sign(self, key: bytes, content: bytes) -> str: + """a function for signing the data""" + pass + + @abstractmethod + def verify(self, key: bytes, content: bytes, signed: bytes) -> bool: + """a function for verifying the data""" + pass diff --git a/src/alvarium/sign/mock.py b/src/alvarium/sign/mock.py new file mode 100644 index 0000000..6f1a8ea --- /dev/null +++ b/src/alvarium/sign/mock.py @@ -0,0 +1,9 @@ +from .interfaces import SignProvider +class NoneSignProvider(SignProvider): + """A mock implementation for the sign provider interface""" + + def sign(self, content: bytes) -> str: + return content.decode("utf-8") + + def verify(self, content: bytes, signed: bytes) -> bool: + return content == signed \ No newline at end of file diff --git a/src/alvarium/streams/__init__.py b/src/alvarium/streams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alvarium/streams/contracts.py b/src/alvarium/streams/contracts.py new file mode 100644 index 0000000..38304f7 --- /dev/null +++ b/src/alvarium/streams/contracts.py @@ -0,0 +1,96 @@ +import json +from enum import Enum +from dataclasses import dataclass +from typing import Any + +class StreamType(Enum): + + MOCK = "mock" + MQTT = "mqtt" + + def __str__(self) -> str: + return f'{self.value}' + +@dataclass +class StreamInfo: + """A data class that encapsulates the type and config of the stream provider""" + + type: StreamType + config: Any + + @staticmethod + def from_json(data: str): + info_json = json.loads(data) + if (info_json["type"] == str(StreamType.MOCK)): + return StreamInfo(type=StreamType(info_json["type"]), config=info_json["config"]) + elif( info_json["type"] == str(StreamType.MQTT)): + return StreamInfo(type=StreamType(info_json["type"]), config=MQTTConfig.from_json( json.dumps(info_json["config"]))) + + def to_json(self) -> str: + info_json = {"type": str(self.type), "config": json.loads(str(self.config))} + return json.dumps(info_json) + + def __str__(self) -> str: + return self.to_json() + +@dataclass +class ServiceInfo: + """A data class that encapsulates the uri related data of a stream provider""" + + host: str + protocol: str + port: int + + def uri(self) -> str: + return f"{self.protocol}://{self.host}:{self.port}" + + @staticmethod + def from_json(data: str): + info_json = json.loads(data) + return ServiceInfo(host=info_json["host"], protocol=info_json["protocol"], port=info_json["port"]) + + def to_json(self) -> str: + info_json = {"host": self.host, "protocol": self.protocol, "port": self.port} + return json.dumps(info_json) + + def __str__(self) -> str: + return self.to_json() + + +@dataclass +class MQTTConfig: + """A data class that encapsulates the MQTT Configuration""" + + client_id: str + user: str + password: str + qos: int + is_clean: bool + topics: list + provider: ServiceInfo + + @staticmethod + def from_json(data: str): + info_json = json.loads(data) + return MQTTConfig(client_id=info_json["clientId"], + user=info_json["user"], + password=info_json["password"], + qos=info_json["qos"], + is_clean=info_json["cleanness"], + topics=info_json["topics"], + provider=ServiceInfo.from_json(json.dumps(info_json["provider"]))) + + def to_json(self) -> str: + info_json = { + "clientId": self.client_id, + "user": self.user, + "password": self.password, + "qos": self.qos, + "cleanness": self.is_clean, + "topics": self.topics, + "provider": json.loads(self.provider.to_json()) + } + return json.dumps(info_json) + + def __str__(self) -> str: + return self.to_json() \ No newline at end of file diff --git a/src/alvarium/streams/exceptions.py b/src/alvarium/streams/exceptions.py new file mode 100644 index 0000000..7052cf3 --- /dev/null +++ b/src/alvarium/streams/exceptions.py @@ -0,0 +1,2 @@ +class StreamException(Exception): + """A custom exception for errors related to streams""" \ No newline at end of file diff --git a/src/alvarium/streams/factories.py b/src/alvarium/streams/factories.py new file mode 100644 index 0000000..929e320 --- /dev/null +++ b/src/alvarium/streams/factories.py @@ -0,0 +1,15 @@ +from .exceptions import StreamException +from .mock import MockProvider +from .mqtt import MQTTStreamProvider +from .contracts import StreamInfo, StreamType + +class StreamProviderFactory: + """A factory that returns a specific implementation of a StreamProvider""" + + def get_provider(self, info: StreamInfo): + if info.type == StreamType.MOCK: + return MockProvider() + if info.type == StreamType.MQTT: + return MQTTStreamProvider(info.config) + else: + raise StreamException(f"{info.type} is not yet implemented.") \ No newline at end of file diff --git a/src/alvarium/streams/interfaces.py b/src/alvarium/streams/interfaces.py new file mode 100644 index 0000000..7ada643 --- /dev/null +++ b/src/alvarium/streams/interfaces.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod +from alvarium.contracts.publish import PublishWrapper + +class StreamProvider(ABC): + """A unit that serves as an interface for a stream provider""" + + @abstractmethod + def connect(self) -> None: + pass + + @abstractmethod + def close(self) -> None: + pass + + @abstractmethod + def publish(self, wrapper: PublishWrapper) -> None: + pass \ No newline at end of file diff --git a/src/alvarium/streams/mock.py b/src/alvarium/streams/mock.py new file mode 100644 index 0000000..a9ec0ff --- /dev/null +++ b/src/alvarium/streams/mock.py @@ -0,0 +1,15 @@ +from .interfaces import StreamProvider +from alvarium.contracts.publish import PublishWrapper + +class MockProvider(StreamProvider): + """mock implementation of the StreamProvider interface""" + + def connect(self) -> None: + pass + + def close(self) -> None: + pass + + def publish(self, wrapper: PublishWrapper) -> None: + wrapper.to_json() + pass \ No newline at end of file diff --git a/src/alvarium/streams/mqtt.py b/src/alvarium/streams/mqtt.py new file mode 100644 index 0000000..60dda4d --- /dev/null +++ b/src/alvarium/streams/mqtt.py @@ -0,0 +1,49 @@ +from alvarium.contracts.publish import PublishWrapper +import paho.mqtt.client as mqtt +from .interfaces import StreamProvider +from .contracts import MQTTConfig +from .exceptions import StreamException + +class MQTTStreamProvider( StreamProvider ): + """implementation of the MQTT StreamProvider""" + + mqttc: mqtt.Client + mqtt_config: MQTTConfig + CONNECTION_TIMEOUT = 2 + + def __init__(self, mqtt_config: MQTTConfig ): + self.mqtt_config = mqtt_config + self.mqttc = mqtt.Client(client_id=self.mqtt_config.client_id, + clean_session=self.mqtt_config.is_clean) + self.mqttc.username_pw_set(self.mqtt_config.user, self.mqtt_config.password) + + def connect(self) -> None: + if (not self.mqttc.is_connected()): + self.mqttc.connect(host=self.mqtt_config.provider.host, keepalive=self.CONNECTION_TIMEOUT) + else: + self.mqttc.reconnect() + + def close(self) -> None: + self.mqttc.disconnect() + + def publish(self, wrapper: PublishWrapper) -> None: + if (not self.mqttc.is_connected()): + self.mqttc.reconnect() + + for topic in self.mqtt_config.topics: + try: + message_info = self.mqttc.publish(topic=topic, + payload=wrapper.to_json(), + qos=self.mqtt_config.qos) + if not message_info.is_published(): + raise StreamException ('Message was not published') + except ValueError as e: + raise StreamException (f"cannot publish to topic {topic}.", e) + + + + + + + + diff --git a/src/alvarium/utils.py b/src/alvarium/utils.py new file mode 100644 index 0000000..6e83cc2 --- /dev/null +++ b/src/alvarium/utils.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod + +class PropertyBag(ABC): + + @abstractmethod + def get_property(self, key: str): + pass + + @abstractmethod + def to_map(self) -> dict: + pass + +class ImmutablePropertyBag(PropertyBag): + + def __init__(self, bag: dict) -> None: + self.bag = bag + + def get_property(self, key: str): + if key in self.bag: + return self.bag.get(key) + else: + raise ValueError(f'Property {key} not found') + + def to_map(self) -> dict: + return self.bag \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..315e00f --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +import os +import sys +PROJECT_PATH = os.getcwd() +SOURCE_PATH = os.path.join( + PROJECT_PATH,"src" +) +sys.path.append(SOURCE_PATH) \ No newline at end of file diff --git a/tests/annotators/__init__.py b/tests/annotators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/annotators/test_annotator.py b/tests/annotators/test_annotator.py new file mode 100644 index 0000000..ea035a3 --- /dev/null +++ b/tests/annotators/test_annotator.py @@ -0,0 +1,26 @@ +import unittest + +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.contracts.config import SdkInfo +from alvarium.hash.contracts import HashInfo, HashType +from alvarium.sign.contracts import KeyInfo, SignInfo, SignType +from alvarium.annotators.factories import AnnotatorFactory +from alvarium.utils import ImmutablePropertyBag + +class AnnotatorTest(unittest.TestCase): + + def test_mock_Annotator_Should_Return_Annotation(self): + key_info = KeyInfo(type=SignType.NONE,path="path") + signature = SignInfo(key_info, key_info) + sdk_info = SdkInfo(annotators=[], hash=HashInfo(type=HashType.MD5), signature=signature, stream=None) + factory = AnnotatorFactory() + annotator = factory.get_annotator(kind=AnnotationType.MOCK,sdk_info=sdk_info) + string = "test data" + data = bytearray(string.encode()) + ctx = ImmutablePropertyBag({}) + annotation = annotator.execute(data=data, ctx=ctx) + + self.assertEqual(type(annotation), Annotation) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/annotators/test_contracts.py b/tests/annotators/test_contracts.py new file mode 100644 index 0000000..f9792ac --- /dev/null +++ b/tests/annotators/test_contracts.py @@ -0,0 +1,34 @@ +import unittest +import json + +from alvarium.annotators.contracts import Signable + +class TestContracts(unittest.TestCase): + + def test_signable_from_json_should_return_signable_object(self): + seed = "this is a seed" + signature = "signature" + test_json = { + "seed": seed, + "signature": signature + } + + result = Signable.from_json(json.dumps(test_json)) + self.assertEqual(result.seed, seed) + self.assertEqual(result.signature, signature) + + def test_signable_to_json_should_return_right_representation(self): + seed = "this is a seed" + signature = "signature" + + signable = Signable(seed=seed, signature=signature) + result = signable.to_json() + result_json = json.loads(result) + + self.assertEqual(result_json["seed"], seed) + self.assertEqual(result_json["signature"], signature) + + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/annotators/test_pki.py b/tests/annotators/test_pki.py new file mode 100644 index 0000000..ecf2dc2 --- /dev/null +++ b/tests/annotators/test_pki.py @@ -0,0 +1,52 @@ +from typing import Type +import unittest + +from alvarium.annotators.factories import AnnotatorFactory +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.contracts.config import SdkInfo +from alvarium.hash.contracts import HashInfo, HashType +from alvarium.sign.contracts import KeyInfo, SignInfo, SignType +from alvarium.annotators.contracts import Signable + +class TestPki(unittest.TestCase): + + def test_pki_execute_should_return_satisfied_annotation(self): + kind = AnnotationType.PKI + hash = HashType.SHA256 + pub_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/public.key") + priv_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key") + sdk_info = SdkInfo(annotators=[], hash=HashInfo(type=hash), stream=None, + signature=SignInfo(public=pub_key, private=priv_key)) + annotator = AnnotatorFactory().get_annotator(kind=kind, sdk_info=sdk_info) + + seed = "helloo" + signature = "B9E41596541933DB7144CFBF72105E4E53F9493729CA66331A658B1B18AC6DF5DA991" + \ + "AD9720FD46A664918DFC745DE2F4F1F8C29FF71209B2DA79DFD1A34F50C" + + test_data = Signable(seed=seed, signature=signature) + annotation = annotator.execute(data=bytes(test_data.to_json(), 'utf-8')) + + self.assertTrue(annotation.is_satisfied) + self.assertEqual(type(annotation), Annotation) + + def test_pki_execute_should_return_unsatisfied_annotation(self): + kind = AnnotationType.PKI + hash = HashType.SHA256 + pub_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/public.key") + priv_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key") + sdk_info = SdkInfo(annotators=[], hash=HashInfo(type=hash), stream=None, + signature=SignInfo(public=pub_key, private=priv_key)) + annotator = AnnotatorFactory().get_annotator(kind=kind, sdk_info=sdk_info) + + seed = "hello" + signature = "B9E41596541933DB7144CFBF72105E4E53F9493729CA66331A658B1B18AC6DF5DA991" + \ + "AD9720FD46A664918DFC745DE2F4F1F8C29FF71209B2DA79DFD1A34F50C" + + test_data = Signable(seed=seed, signature=signature) + annotation = annotator.execute(data=bytes(test_data.to_json(), 'utf-8')) + + self.assertFalse(annotation.is_satisfied) + self.assertEqual(type(annotation), Annotation) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/annotators/test_source.py b/tests/annotators/test_source.py new file mode 100644 index 0000000..49b17cd --- /dev/null +++ b/tests/annotators/test_source.py @@ -0,0 +1,29 @@ +import unittest +from alvarium.annotators.factories import AnnotatorFactory +from alvarium.contracts.annotation import AnnotationType +from alvarium.contracts.config import SdkInfo +from alvarium.hash.contracts import HashInfo, HashType +from alvarium.sign.contracts import KeyInfo, SignInfo, SignType + +class SourceAnnotatorTest(unittest.TestCase): + + def test_execute_should_return_annotation(self): + hash = HashType.SHA256 + kind = AnnotationType.SOURCE + private_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key") + public_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/public.key") + sign_info = SignInfo(private=private_key, public=public_key) + sdk_info = SdkInfo(annotators=[], hash=HashInfo(type=hash), signature=sign_info, stream=[]) + + annotator = AnnotatorFactory().get_annotator(kind=kind, sdk_info=sdk_info) + + test_data = b"test data" + result = annotator.execute(data=test_data) + self.assertEqual(kind, result.kind) + self.assertEqual(hash, result.hash) + self.assertIsNotNone(result.signature) + # Source annotator is_satisfied should always be true + self.assertEqual(True, result.is_satisfied) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/annotators/test_tls.py b/tests/annotators/test_tls.py new file mode 100644 index 0000000..2403256 --- /dev/null +++ b/tests/annotators/test_tls.py @@ -0,0 +1,53 @@ +import unittest + +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.contracts.config import SdkInfo +from alvarium.hash.contracts import HashInfo, HashType +from alvarium.sign.contracts import KeyInfo, SignInfo, SignType +from alvarium.annotators.factories import AnnotatorFactory +from alvarium.utils import ImmutablePropertyBag +import ssl +import socket + +class TlsAnnotatorTest(unittest.TestCase): + def test_tls_annotator_with_tls_connection_should_return_is_satisfied_true(self): + key_info = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key") + sign_Info = SignInfo(public=key_info, private=key_info) + sdk_info = SdkInfo(annotators=[], signature=sign_Info, hash=HashInfo(type=HashType.SHA256), stream=None) + factory = AnnotatorFactory() + annotator = factory.get_annotator(kind=AnnotationType.TLS, sdk_info=sdk_info) + string = "test data" + data = bytes(string, 'utf-8') + + hostname = 'google.com' + context = ssl.create_default_context() + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname, do_handshake_on_connect=False) as ssock: + ctx = ImmutablePropertyBag({str(AnnotationType.TLS): ssock}) + annotation = annotator.execute(data=data, ctx=ctx) + + self.assertEqual(annotation.is_satisfied, True) + self.assertEqual(type(annotation), Annotation) + + + def test_tls_annotator_without_tls_connection_should_return_is_satisfied_false(self): + key_info = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key") + sign_Info = SignInfo(public=key_info, private=key_info) + sdk_info = SdkInfo(annotators=[], signature=sign_Info, hash=HashInfo(type=HashType.SHA256), stream=None) + factory = AnnotatorFactory() + annotator = factory.get_annotator(kind=AnnotationType.TLS, sdk_info=sdk_info) + string = "test data" + data = bytes(string, 'utf-8') + + #Handshake fails in this website so tls shoudl be false + hostname = 'googel.com' + context = ssl.create_default_context() + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname, do_handshake_on_connect=False) as ssock: + ctx = ImmutablePropertyBag({str(AnnotationType.TLS): ssock}) + annotation = annotator.execute(data=data, ctx=ctx) + + self.assertEqual(annotation.is_satisfied, False) + self.assertEqual(type(annotation), Annotation) \ No newline at end of file diff --git a/tests/annotators/test_tpm.py b/tests/annotators/test_tpm.py new file mode 100644 index 0000000..b9228d9 --- /dev/null +++ b/tests/annotators/test_tpm.py @@ -0,0 +1,30 @@ +import unittest +from alvarium.annotators.factories import AnnotatorFactory +from alvarium.contracts.annotation import AnnotationType +from alvarium.contracts.config import SdkInfo +from alvarium.hash.contracts import HashInfo, HashType +from alvarium.sign.contracts import KeyInfo, SignInfo, SignType + +# implementing tests for is_satisfied in the tpm case is omitted +# as the test will not be platform agnostic +class TestTpmAnnotator(unittest.TestCase): + + def test_execute_should_return_annotation(self): + hash = HashType.SHA256 + kind = AnnotationType.TPM + private_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key") + public_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/public.key") + sign_info = SignInfo(private=private_key, public=public_key) + sdk_info = SdkInfo(annotators=[], signature=sign_info, hash=HashInfo(type=hash), stream=[]) + + annotator = AnnotatorFactory().get_annotator(kind=kind, sdk_info=sdk_info) + + test_data = b"test data" + result = annotator.execute(data=test_data) + self.assertEqual(kind, result.kind) + self.assertEqual(hash, result.hash) + self.assertIsNotNone(result.signature) + self.assertIsNotNone(result.is_satisfied) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/annotators/test_utils.py b/tests/annotators/test_utils.py new file mode 100644 index 0000000..7780294 --- /dev/null +++ b/tests/annotators/test_utils.py @@ -0,0 +1,38 @@ +import unittest + +from alvarium.annotators.utils import derive_hash, sign_annotation +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.hash.contracts import HashType +from alvarium.sign.contracts import KeyInfo, SignType +from alvarium.sign.factories import SignProviderFactory +from alvarium.sign.interfaces import SignProvider + +class TestUtils(unittest.TestCase): + + def test_derive_hash_should_return_the_right_hash(self): + data = "this is a test" + expected = "2e99758548972a8e8822ad47fa1017ff72f06f3ff6a016851f45c398732bc50c" #ground truth + result: str = derive_hash(hash=HashType.SHA256, data=bytes(data, 'utf-8')) + + self.assertEqual(expected, result) + + def test_sign_annotation_should_return_signature(self): + factory = SignProviderFactory() + sign_provider: SignProvider = factory.get_provider(SignType.ED25519) + + private_key = KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key") + annotation = Annotation(key="key", hash="hash", host="host", kind=AnnotationType.MOCK, + is_satisfied=True) + + result = sign_annotation(key_info=private_key, annotation=annotation) + + with open(f"./tests/sign/keys/public.key", "r") as file: + public_key_hex = file.read() + public_key_bytes = bytes.fromhex(public_key_hex) + + self.assertTrue(sign_provider.verify(key=public_key_bytes,content=bytes(annotation.to_json(), 'utf-8'), + signed=bytes.fromhex(result))) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/contracts/__init__.py b/tests/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/contracts/annotation.json b/tests/contracts/annotation.json new file mode 100644 index 0000000..b3345bb --- /dev/null +++ b/tests/contracts/annotation.json @@ -0,0 +1,10 @@ +{ + "id": "01FE0RFFJY94R1ER2AM9BG63E2", + "key": "KEY", + "hash": "none", + "host": "host", + "kind": "mock", + "signature": "SIGN", + "isSatisfied": true, + "timestamp": "2021-08-24T12:22:33.334070489-05:00" +} diff --git a/tests/contracts/annotation_list.json b/tests/contracts/annotation_list.json new file mode 100644 index 0000000..5e15cd4 --- /dev/null +++ b/tests/contracts/annotation_list.json @@ -0,0 +1,24 @@ +{ + "items": [ + { + "id": "01FE0RFFJY94R1ER2AM9BG63E2", + "key": "KEY", + "hash": "none", + "host": "host", + "kind": "mock", + "signature": "SIGN", + "isSatisfied": true, + "timestamp": "2021-08-24T12:22:33.334070489-05:00" + }, + { + "id": "01FE0RFFJY94R1ER2AM9BG63E2", + "key": "KEY", + "hash": "none", + "host": "host", + "kind": "mock", + "signature": "SIGN", + "isSatisfied": true, + "timestamp": "2021-08-24T12:22:33.334070489-05:00" + } + ] +} diff --git a/tests/contracts/sdk-info.json b/tests/contracts/sdk-info.json new file mode 100644 index 0000000..2e08f63 --- /dev/null +++ b/tests/contracts/sdk-info.json @@ -0,0 +1,32 @@ +{ + "annotators": ["tls", "mock"], + "hash": { + "type": "sha256" + }, + "signature": { + "public": { + "type": "ed25519", + "path": "./tests/sign/keys/public.key" + }, + "private": { + "type": "ed25519", + "path": "./tests/sign/keys/private.key" + } + }, + "stream": { + "type": "mqtt", + "config": { + "clientId": "alvarium-test", + "qos": 0, + "user": "", + "password": "", + "provider": { + "host": "test.mosquitto.org", + "protocol": "tcp", + "port": 1883 + }, + "cleanness": false, + "topics": ["alvarium-test-topic"] + } + } +} diff --git a/tests/contracts/test_annotation.py b/tests/contracts/test_annotation.py new file mode 100644 index 0000000..8de0275 --- /dev/null +++ b/tests/contracts/test_annotation.py @@ -0,0 +1,48 @@ +import json +import unittest +from alvarium.contracts.annotation import Annotation, AnnotationType +from alvarium.hash.contracts import HashType + +class TestAnnotation(unittest.TestCase): + + def test_to_json_should_return_json_representation(self): + annotation = Annotation(key="KEY", hash=HashType.NONE, host="host", + kind=AnnotationType.MOCK,signature= "SIGN", is_satisfied=True) + test_json = {} + with open("./tests/contracts/annotation.json", "r") as file: + test_json = json.loads(file.read()) + + result = json.loads(annotation.to_json()) + self.assertEqual(test_json["key"], result["key"]) + self.assertEqual(test_json["hash"], result["hash"]) + self.assertEqual(test_json["host"], result["host"]) + self.assertEqual(test_json["kind"], result["kind"]) + self.assertEqual(test_json["kind"], result["kind"]) + self.assertEqual(test_json["signature"], result["signature"]) + self.assertEqual(test_json["isSatisfied"], result["isSatisfied"]) + + def test_to_json_should_omit_empty_signature(self): + annotation = Annotation(key="KEY", hash=HashType.NONE, host="host", + kind=AnnotationType.MOCK, is_satisfied=True) + + annotation_json = json.loads(annotation.to_json()) + self.assertRaises(KeyError, lambda: annotation_json["signature"]) + + + def test_from_json_should_return_annotation_object(self): + test_json = "" + with open("./tests/contracts/annotation.json", "r") as file: + test_json = file.read() + + result = Annotation.from_json(test_json) + self.assertEqual(result.id, "01FE0RFFJY94R1ER2AM9BG63E2") + self.assertEqual(result.key, "KEY") + self.assertEqual(result.hash, HashType.NONE) + self.assertEqual(result.host, "host") + self.assertEqual(result.kind, AnnotationType.MOCK) + self.assertEqual(result.signature, "SIGN") + self.assertEqual(result.is_satisfied, True) + self.assertEqual(result.timestamp, "2021-08-24T12:22:33.334070489-05:00") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/contracts/test_annotation_list.py b/tests/contracts/test_annotation_list.py new file mode 100644 index 0000000..22ca94e --- /dev/null +++ b/tests/contracts/test_annotation_list.py @@ -0,0 +1,27 @@ +import json +from typing import List +import unittest + +from alvarium.contracts.annotation import Annotation, AnnotationList, AnnotationType +from alvarium.hash.contracts import HashType + +class TestAnnotationList(unittest.TestCase): + + def test_to_json_should_return_json_representation(self): + annotation = Annotation(key="KEY", hash=HashType.NONE, host="host", + kind=AnnotationType.MOCK,signature= "SIGN" ,is_satisfied=True) + annotation_list = AnnotationList(items=[annotation, annotation]) + result = annotation_list.to_json() + self.assertEqual(type(result), str) + + def test_from_json_should_return_annotation_list_object(self): + test_json = "" + with open("./tests/contracts/annotation_list.json", "r") as file: + test_json = file.read() + + annotation_list = AnnotationList.from_json(test_json) + self.assertEqual(type(annotation_list.items[0]), Annotation) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/contracts/test_config.py b/tests/contracts/test_config.py new file mode 100644 index 0000000..a3c5674 --- /dev/null +++ b/tests/contracts/test_config.py @@ -0,0 +1,56 @@ +import unittest +import json +from alvarium.contracts.annotation import AnnotationType + +from alvarium.contracts.config import SdkInfo +from alvarium.hash.contracts import HashInfo, HashType +from alvarium.sign.contracts import KeyInfo, SignInfo, SignType +from alvarium.streams.contracts import MQTTConfig, ServiceInfo, StreamInfo, StreamType + +class TestConfig(unittest.TestCase): + + def test_sdk_info_from_json_should_return_sdk_info(self): + annotators = [AnnotationType.TLS, AnnotationType.MOCK] + hash = HashInfo(type=HashType.SHA256) + signature = SignInfo(public=KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/public.key"), + private=KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key")) + stream = StreamInfo(type=StreamType.MQTT, config=MQTTConfig(client_id="alvarium-test", qos=0, user="", + password="", is_clean=False, topics=["alvarium-test-topic"], provider=ServiceInfo( + host="test.mosquitto.org", port=1883, protocol="tcp" + ))) + + test_json = {} + with open("./tests/contracts/sdk-info.json", "r") as file: + test_json = json.loads(file.read()) + + result = SdkInfo.from_json(json.dumps(test_json)) + self.assertEqual(result.annotators, annotators) + self.assertEqual(result.hash, hash) + self.assertEqual(result.signature, signature) + self.assertEqual(result.stream, stream) + + def test_sdk_info_to_json_should_return_right_representation(self): + annotators = [AnnotationType.TLS, AnnotationType.MOCK] + hash = HashInfo(type=HashType.SHA256) + signature = SignInfo(public=KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/public.key"), + private=KeyInfo(type=SignType.ED25519, path="./tests/sign/keys/private.key")) + stream = StreamInfo(type=StreamType.MQTT, config=MQTTConfig(client_id="alvarium-test", qos=0, user="", + password="", is_clean=False, topics=["alvarium-test-topic"], provider=ServiceInfo( + host="test.mosquitto.org", port=1883, protocol="tcp" + ))) + + sdk_info = SdkInfo(annotators=annotators, hash=hash, signature=signature, stream=stream) + result = json.loads(sdk_info.to_json()) + + test_json = {} + with open("./tests/contracts/sdk-info.json", "r") as file: + test_json = json.loads(file.read()) + + self.assertEqual(result["annotators"], test_json["annotators"]) + self.assertEqual(result["hash"], test_json["hash"]) + self.assertEqual(result["signature"], test_json["signature"]) + self.assertEqual(result["stream"], test_json["stream"]) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/contracts/test_publish.py b/tests/contracts/test_publish.py new file mode 100644 index 0000000..86ee5a1 --- /dev/null +++ b/tests/contracts/test_publish.py @@ -0,0 +1,27 @@ +import base64 +import unittest +import json +from alvarium.contracts.annotation import Annotation, AnnotationList, AnnotationType + +from alvarium.contracts.publish import PublishWrapper, SdkAction +from alvarium.hash.contracts import HashType + +class TestPublishWrapper(unittest.TestCase): + + def test_to_json_should_return_json_representation(self): + annotation = Annotation(key="key", hash=HashType.NONE, host="host", kind=AnnotationType.MOCK, + is_satisfied=True, signature="signature") + action = SdkAction.CREATE + content = AnnotationList(items=[annotation, annotation]) + message_type = str(type(content)) + wrapper = PublishWrapper(action=action, message_type=message_type, content=content) + + result = wrapper.to_json() + wrapper_json = json.loads(result) + + self.assertEqual(SdkAction(wrapper_json["action"]), action) + self.assertEqual(wrapper_json["messageType"], message_type) + self.assertEqual(wrapper_json["content"], base64.b64encode(bytes(str(content), 'utf-8')).decode('utf-8')) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/example/__main__.py b/tests/example/__main__.py new file mode 100644 index 0000000..a2e52b8 --- /dev/null +++ b/tests/example/__main__.py @@ -0,0 +1,55 @@ +# set project directory +import os +import sys + +PROJECT_PATH = os.getcwd() +SOURCE_PATH = os.path.join( + PROJECT_PATH,"src" +) +sys.path.append(SOURCE_PATH) + + +# ------------------------ app starts here ----------------------- # +import json, logging + +from typing import List +from alvarium.contracts.config import SdkInfo +from alvarium.annotators.interfaces import Annotator +from alvarium.annotators.factories import AnnotatorFactory +from alvarium.default import DefaultSdk +from alvarium.annotators.contracts import Signable + +with open("./tests/example/sdk-config.json", "r") as file: + config = json.loads(file.read()) + +# construct sdk config +sdk_info = SdkInfo.from_json(json.dumps(config["sdk"])) + +# construct logger +logger = logging.getLogger(__name__) +logging.basicConfig(level = logging.DEBUG) + +# construct annotators +annotator_factory = AnnotatorFactory() +annotators: List[Annotator] = [annotator_factory.get_annotator(kind=annotation_type, sdk_info=sdk_info) \ + for annotation_type in sdk_info.annotators] + +# construct sdk +sdk = DefaultSdk(annotators=annotators, logger=logger, config=sdk_info) + +# construct sample data +# in this case, we'll use the Signable data class to ensure that the pki annotator +# will return satisfied annotation +signable = Signable(seed="helloo", + signature="B9E41596541933DB7144CFBF72105E4E53F9493729CA66331A658B1B18AC6DF5DA991" + \ + "AD9720FD46A664918DFC745DE2F4F1F8C29FF71209B2DA79DFD1A34F50C") +old_data = bytes(signable.to_json(), 'utf-8') +new_data = bytes(signable.to_json(), 'utf-8') + +# call sdk methods +sdk.create(data=old_data) +sdk.mutate(old_data=old_data, new_data=new_data) +sdk.transit(data=new_data) + +# dispose sdk +sdk.close() \ No newline at end of file diff --git a/tests/example/private.key b/tests/example/private.key new file mode 100644 index 0000000..277fd9e --- /dev/null +++ b/tests/example/private.key @@ -0,0 +1 @@ +9e7d6234a79fe6af5d4880c73dfad50312b87247e949248a42ffbe5c32f8172d5e71ef8d30b9e028ddd8f2654d48ef665b27f18c186d645ce204d4288b3d3bd4 \ No newline at end of file diff --git a/tests/example/public.key b/tests/example/public.key new file mode 100644 index 0000000..f5e20ef --- /dev/null +++ b/tests/example/public.key @@ -0,0 +1 @@ +5e71ef8d30b9e028ddd8f2654d48ef665b27f18c186d645ce204d4288b3d3bd4 \ No newline at end of file diff --git a/tests/example/sdk-config.json b/tests/example/sdk-config.json new file mode 100644 index 0000000..8898565 --- /dev/null +++ b/tests/example/sdk-config.json @@ -0,0 +1,37 @@ +{ + "sdk": { + "annotators": ["tpm", "pki"], + "hash": { + "type": "sha256" + }, + "signature": { + "public": { + "type": "ed25519", + "path": "./tests/example/public.key" + }, + "private": { + "type": "ed25519", + "path": "./tests/example/private.key" + } + }, + "stream": { + "type": "mqtt", + "config": { + "clientId": "alvarium-test", + "qos": 0, + "user": "mosquitto", + "password": "", + "provider": { + "host": "localhost", + "protocol": "tcp", + "port": 1883 + }, + "cleanness": false, + "topics": ["alvarium-test-topic"] + } + } + }, + "logging": { + "minLogLevel": "debug" + } +} diff --git a/tests/hash/__init__.py b/tests/hash/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/hash/hash_info.json b/tests/hash/hash_info.json new file mode 100644 index 0000000..0d7e7be --- /dev/null +++ b/tests/hash/hash_info.json @@ -0,0 +1,3 @@ +{ + "type": "md5" +} diff --git a/tests/hash/test_hash_info.py b/tests/hash/test_hash_info.py new file mode 100644 index 0000000..868b931 --- /dev/null +++ b/tests/hash/test_hash_info.py @@ -0,0 +1,26 @@ +import json +import unittest + +from alvarium.hash.contracts import HashInfo, HashType + +class TestHashInfo(unittest.TestCase): + + def test_to_json_should_return_json_representation(self): + hash_info = HashInfo(type=HashType.MD5) + test_json = {} + with open("./tests/hash/hash_info.json", "r") as file: + test_json = json.loads(file.read()) + + result = hash_info.to_json() + self.assertEqual(json.loads(result), test_json) + + def test_from_json_should_return_hash_info(self): + test_json = "" + with open("./tests/hash/hash_info.json", "r") as file: + test_json = file.read() + + hash_info = HashInfo.from_json(data=test_json) + self.assertEqual(hash_info.type, HashType.MD5) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/hash/test_hash_provider.py b/tests/hash/test_hash_provider.py new file mode 100644 index 0000000..b3b6810 --- /dev/null +++ b/tests/hash/test_hash_provider.py @@ -0,0 +1,39 @@ +import unittest + +from alvarium.hash.contracts import HashType + +from alvarium.hash.factories import HashProviderFactory +from alvarium.hash.interfaces import HashProvider + +class TestHashProvider(unittest.TestCase): + + def test_none_provider_should_return_data(self): + factory = HashProviderFactory() + hash_provider: HashProvider = factory.get_provider(HashType.NONE) + + test_string = "this is a test" + result = hash_provider.derive(data=bytes(test_string, "utf-8")) + self.assertEqual(result, test_string) + + def test_sha256_provider_should_return_hex_of_hashed_data(self): + factory = HashProviderFactory() + hash_provider: HashProvider = factory.get_provider(HashType.SHA256) + + test_string = b"this is a test" + test_string_hex = "2e99758548972a8e8822ad47fa1017ff72f06f3ff6a016851f45c398732bc50c" #ground truth + result = hash_provider.derive(data=test_string) + self.assertEqual(result, test_string_hex) + + def test_md5_provider_should_return_hex_of_hashed_data(self): + factory = HashProviderFactory() + hash_provider: HashProvider = factory.get_provider(HashType.MD5) + + test_string = b"this is a test" + test_string_hex = "54b0c58c7ce9f2a8b551351102ee0938" #ground truth + result = hash_provider.derive(data=test_string) + self.assertEqual(result, test_string_hex) + + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/mock-info.json b/tests/mock-info.json new file mode 100644 index 0000000..7dc4975 --- /dev/null +++ b/tests/mock-info.json @@ -0,0 +1,21 @@ +{ + "annotators": ["mock", "mock"], + "hash": { + "type": "sha256" + }, + "signature": { + "public": { + "type": "ed25519", + "path": "./tests/sign/keys/public.key" + }, + "private": { + "type": "ed25519", + "path": "./tests/sign/keys/private.key" + } + }, + "stream": { + "type": "mock", + "config": { + } + } +} diff --git a/tests/sign/__init__.py b/tests/sign/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/sign/keys/private.key b/tests/sign/keys/private.key new file mode 100644 index 0000000..277fd9e --- /dev/null +++ b/tests/sign/keys/private.key @@ -0,0 +1 @@ +9e7d6234a79fe6af5d4880c73dfad50312b87247e949248a42ffbe5c32f8172d5e71ef8d30b9e028ddd8f2654d48ef665b27f18c186d645ce204d4288b3d3bd4 \ No newline at end of file diff --git a/tests/sign/keys/public.key b/tests/sign/keys/public.key new file mode 100644 index 0000000..f5e20ef --- /dev/null +++ b/tests/sign/keys/public.key @@ -0,0 +1 @@ +5e71ef8d30b9e028ddd8f2654d48ef665b27f18c186d645ce204d4288b3d3bd4 \ No newline at end of file diff --git a/tests/sign/sign_info.json b/tests/sign/sign_info.json new file mode 100644 index 0000000..5b88dd5 --- /dev/null +++ b/tests/sign/sign_info.json @@ -0,0 +1,10 @@ +{ + "public": { + "type": "ed25519", + "path": "/path" + }, + "private": { + "type": "none", + "path": "/path" + } + } \ No newline at end of file diff --git a/tests/sign/test_sign_info.py b/tests/sign/test_sign_info.py new file mode 100644 index 0000000..a8027c1 --- /dev/null +++ b/tests/sign/test_sign_info.py @@ -0,0 +1,28 @@ +from os import path +import unittest +from alvarium.sign.contracts import SignInfo, SignType, KeyInfo +import json +class TestSignInfo(unittest.TestCase): + + def test_to_json_should_return_josn_representation(self): + public_key_info = KeyInfo(type=SignType.ED25519, path="/path") + private_key_info = KeyInfo(type=SignType.NONE, path="/path") + + sign_info = SignInfo(public=public_key_info, private=private_key_info) + + test_json = "" + with open("./tests/sign/sign_info.json", "r") as file: + test_json = json.loads(file.read()) + + result = sign_info.to_json() + + self.assertEqual(json.loads(result), test_json) + + def test_from_json_should_return_sign_info(self): + sign_json = "" + with open("./tests/sign/sign_info.json", "r") as file: + sign_json = file.read() + + sign_info = SignInfo.from_json(data=sign_json) + + self.assertEqual(sign_info.public.type, SignType.ED25519) \ No newline at end of file diff --git a/tests/sign/test_sign_provider.py b/tests/sign/test_sign_provider.py new file mode 100644 index 0000000..e0d95ee --- /dev/null +++ b/tests/sign/test_sign_provider.py @@ -0,0 +1,105 @@ +import unittest +from alvarium.sign.factories import SignProviderFactory +from alvarium.sign.contracts import SignType +from alvarium.sign.interfaces import SignProvider +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives import serialization + + +class TestSignProvider(unittest.TestCase): + + def test_none_sign_provider_should_return_data_signed(self): + factory = SignProviderFactory() + sign_provider: SignProvider = factory.get_provider(SignType.NONE) + + test_string = "Signing test string" + result = sign_provider.sign(content=bytes(test_string, "utf-8")) + + self.assertEqual(result, test_string) + + def test_none_sign_provider_should_verify_true_signed_content(self): + factory = SignProviderFactory() + sign_provider: SignProvider = factory.get_provider(SignType.NONE) + + test_string = bytes("Signed Test String", "utf-8") + signed_test_string = bytes("Signed Test String", "utf-8") + + result = sign_provider.verify(content=test_string, signed=signed_test_string) + + self.assertTrue(result) + + def test_none_sign_provider_should_verify_false_signed_content(self): + factory = SignProviderFactory() + sign_provider: SignProvider = factory.get_provider(SignType.NONE) + + test_string = bytes("Signed Test String", "utf-8") + changed_signed_test_string = bytes( + "Changed Signed Test String", "utf-8") + + result = sign_provider.verify(content=test_string, signed=changed_signed_test_string) + + self.assertFalse(result) + + def test_ed25519_sign_provider_should_return_true(self): + factory = SignProviderFactory() + sign_provider: SignProvider = factory.get_provider(SignType.ED25519) + + private_key = Ed25519PrivateKey.generate() + private_key_bytes = private_key.private_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption() + ) + public_key_bytes = private_key.public_key().public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + + test_string = "Signing test string" + signed_test_string = sign_provider.sign(key=private_key_bytes, content=bytes(test_string, "utf-8")) + verification_status = sign_provider.verify(key=public_key_bytes, content=bytes(test_string, "utf-8"), + signed=bytes.fromhex(signed_test_string)) + + self.assertEqual(verification_status, True) + + def test_ed25519_sign_provider_should_return_false(self): + factory = SignProviderFactory() + sign_provider: SignProvider = factory.get_provider(SignType.ED25519) + + private_key = Ed25519PrivateKey.generate() + + private_key_bytes = private_key.private_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption() + ) + + wrong_public_key_bytes = Ed25519PrivateKey.generate().public_key().public_bytes(encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + test_string = "Signing test string" + signed_test_string = sign_provider.sign(key=private_key_bytes, content=bytes(test_string, "utf-8")) + verification_status = sign_provider.verify(key=wrong_public_key_bytes, content=bytes(test_string, "utf-8"), + signed=bytes.fromhex(signed_test_string)) + + self.assertEqual(verification_status, False) + + def test_ed25519_sign_provider_with_loaded_key_should_return_true(self): + factory = SignProviderFactory() + sign_provider: SignProvider = factory.get_provider(SignType.ED25519) + + with open(f"./tests/sign/keys/public.key", "r") as key_file: + public_key_hex = key_file.read() + public_key_bytes = bytes.fromhex( public_key_hex ) + + with open(f"./tests/sign/keys/private.key", "r") as key_file: + private_key_hex = key_file.read() + private_key_bytes = bytes.fromhex( private_key_hex ) + + test_string = "Signing test string" + signed_test_string = sign_provider.sign(key=private_key_bytes, content=bytes(test_string, "utf-8")) + verification_status = sign_provider.verify(key=public_key_bytes, content=bytes(test_string, "utf-8"), + signed=bytes.fromhex(signed_test_string)) + + self.assertEqual(verification_status, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/streams/__init__.py b/tests/streams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/streams/config-mqtt.json b/tests/streams/config-mqtt.json new file mode 100644 index 0000000..82301f0 --- /dev/null +++ b/tests/streams/config-mqtt.json @@ -0,0 +1,16 @@ +{ + "type": "mqtt", + "config": { + "clientId": "alvarium-test", + "qos": 0, + "user": "", + "password": "", + "provider": { + "host": "localhost", + "protocol": "tcp", + "port": 1883 + }, + "cleanness": false, + "topics": ["alvarium-test-topic"] + } +} diff --git a/tests/streams/service-info.json b/tests/streams/service-info.json new file mode 100644 index 0000000..a96cb83 --- /dev/null +++ b/tests/streams/service-info.json @@ -0,0 +1,5 @@ +{ + "host": "localhost", + "protocol": "tcp", + "port": 1883 +} diff --git a/tests/streams/test_contracts.py b/tests/streams/test_contracts.py new file mode 100644 index 0000000..22cffa1 --- /dev/null +++ b/tests/streams/test_contracts.py @@ -0,0 +1,64 @@ +import unittest +import json + +from alvarium.streams.contracts import ServiceInfo, StreamInfo, StreamType + +class TestStreamInfo(unittest.TestCase): + + def test_to_json_should_return_json_representation(self): + stream_info = StreamInfo(type=StreamType.MQTT, config={}) + result = stream_info.to_json() + + info_json = json.loads(result) + self.assertEqual(StreamType(info_json["type"]), StreamType.MQTT) + self.assertEqual(info_json["config"], {}) + + def test_from_json_should_return_stream_info(self): + info_json = {} + + with open("./tests/streams/config-mqtt.json") as file: + info_json = json.loads(file.read()) + + stream_info = StreamInfo.from_json(json.dumps(info_json)) + self.assertEqual(StreamType.MQTT, stream_info.type) + +class TestServiceInfo(unittest.TestCase): + + def test_to_json_should_return_json_representation(self): + host = "localhost" + protocol = "tcp" + port = 1883 + service_info = ServiceInfo(host=host, protocol=protocol, port=port) + result = service_info.to_json() + + info_json = json.loads(result) + self.assertEqual(host, info_json["host"]) + self.assertEqual(protocol, info_json["protocol"]) + self.assertEqual(port, info_json["port"]) + + def test_from_json_should_return_service_info_object(self): + host = "localhost" + protocol = "tcp" + port = 1883 + info_json = {} + with open("./tests/streams/service-info.json") as file: + info_json = json.loads(file.read()) + + result = ServiceInfo.from_json(json.dumps(info_json)) + + self.assertEqual(result.host, host) + self.assertEqual(result.protocol, protocol) + self.assertEqual(result.port, port) + + def test_uri_should_return_the_right_representation(self): + host = "localhost" + protocol = "tcp" + port = 1883 + + service_info = ServiceInfo(host=host, protocol=protocol, port=port) + uri = service_info.uri() + self.assertEqual(f"{protocol}://{host}:{port}", uri) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/streams/test_mqtt_provider.py b/tests/streams/test_mqtt_provider.py new file mode 100644 index 0000000..97af1be --- /dev/null +++ b/tests/streams/test_mqtt_provider.py @@ -0,0 +1,66 @@ +import unittest + +from alvarium.streams.contracts import StreamInfo +from alvarium.streams.factories import StreamProviderFactory +from alvarium.contracts.publish import PublishWrapper, SdkAction + +class TestMQTTStreamProvider(unittest.TestCase): + + def should_load_config(self): + config_path = 'tests/streams/config-mqtt.json' + + with open(config_path, "r") as file: + config_json = file.read() + + stream_info = StreamInfo.from_json(config_json) + mqtt_config = stream_info.config + self.assertIsNotNone(mqtt_config) + + def mqtt_should_connect(self): + config_path = 'tests/streams/config-mqtt.json' + + with open(config_path, "r") as file: + config_json = file.read() + + stream_info = StreamInfo.from_json(config_json) + + factory = StreamProviderFactory() + provider = factory.get_provider(info=stream_info) + + provider.connect() + + def mqtt_should_publish(self): + config_path = 'tests/streams/config-mqtt.json' + + with open(config_path, "r") as file: + config_json = file.read() + + stream_info = StreamInfo.from_json(config_json) + + factory = StreamProviderFactory() + provider = factory.get_provider(info=stream_info) + + provider.connect() + msg = '{"content" : "connected"}' + test_publish_wrapper = PublishWrapper(SdkAction.CREATE, message_type="str", content=msg) + + provider.publish(wrapper=test_publish_wrapper) + + def mqtt_should_close(self): + config_path = 'tests/streams/config-mqtt.json' + + with open(config_path, "r") as file: + config_json = file.read() + + stream_info = StreamInfo.from_json(config_json) + + factory = StreamProviderFactory() + provider = factory.get_provider(info=stream_info) + + provider.connect() + + msg = '{"content" : "closing"}' + test_publish_wrapper = PublishWrapper(SdkAction.CREATE, message_type="str", content= msg) + + provider.publish(wrapper=test_publish_wrapper) + provider.close() \ No newline at end of file diff --git a/tests/streams/test_stream_provider.py b/tests/streams/test_stream_provider.py new file mode 100644 index 0000000..5f25b7a --- /dev/null +++ b/tests/streams/test_stream_provider.py @@ -0,0 +1,38 @@ +import unittest + +from alvarium.streams.contracts import StreamInfo, StreamType +from alvarium.streams.factories import StreamProviderFactory +from alvarium.contracts.publish import PublishWrapper, SdkAction + +class TestStreamProvider(unittest.TestCase): + + def test_mock_provider_connect_should_return_none(self): + stream_info = StreamInfo(type=StreamType.MOCK, config={}) + + factory = StreamProviderFactory() + provider = factory.get_provider(info=stream_info) + + result = provider.connect() + self.assertEqual(result, None) + + def test_mock_provider_close_should_return_none(self): + stream_info = StreamInfo(type=StreamType.MOCK, config={}) + + factory = StreamProviderFactory() + provider = factory.get_provider(info=stream_info) + + result = provider.close() + self.assertEqual(result, None) + + def test_mock_provider_publish_should_return_none(self): + stream_info = StreamInfo(type=StreamType.MOCK, config={}) + wrapper = PublishWrapper(action=SdkAction.CREATE, message_type=str(dict), content={}) + + factory = StreamProviderFactory() + provider = factory.get_provider(info=stream_info) + + result = provider.publish(wrapper=wrapper) + self.assertEqual(result, None) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_sdk.py b/tests/test_sdk.py new file mode 100644 index 0000000..32f88f6 --- /dev/null +++ b/tests/test_sdk.py @@ -0,0 +1,82 @@ +import unittest, json, logging +from typing import List +from alvarium.contracts.config import SdkInfo +from alvarium.annotators.factories import AnnotatorFactory +from alvarium.sdk import Sdk +from alvarium.default import DefaultSdk + +class TestSdk(unittest.TestCase): + + def setUp(self) -> None: + self.test_json = {} + with open("./tests/mock-info.json", "r") as file: + self.test_json = json.loads(file.read()) + + def test_default_sdk_instantiate_should_not_raise(self): + sdk_info: SdkInfo = SdkInfo.from_json(json.dumps(self.test_json)) + annotator_factory = AnnotatorFactory() + annotators = [annotator_factory.get_annotator(kind=annotation_type, sdk_info=sdk_info) for annotation_type in sdk_info.annotators] + + logger = logging.getLogger(__name__) + logging.basicConfig(level = logging.DEBUG) + + sdk: Sdk = DefaultSdk(annotators=annotators,config=sdk_info,logger=logger) + sdk.close() + + def test_sdk_should_create(self): + sdk_info: SdkInfo = SdkInfo.from_json(json.dumps(self.test_json)) + annotator_factory = AnnotatorFactory() + annotators = [annotator_factory.get_annotator(kind=annotation_type, sdk_info=sdk_info) for annotation_type in sdk_info.annotators] + + logger = logging.getLogger(__name__) + logging.basicConfig(level = logging.DEBUG) + sdk = DefaultSdk(annotators=annotators, config=sdk_info, logger=logger) + + test_data = b'test' + sdk.create(data=test_data) + sdk.close() + + def test_sdk_should_mutate(self): + sdk_info: SdkInfo = SdkInfo.from_json(json.dumps(self.test_json)) + annotator_factory = AnnotatorFactory() + annotators = [annotator_factory.get_annotator(kind=annotation_type, sdk_info=sdk_info) for annotation_type in sdk_info.annotators] + + logger = logging.getLogger(__name__) + logging.basicConfig(level = logging.DEBUG) + sdk = DefaultSdk(annotators=annotators, config=sdk_info, logger=logger) + + old_data = b'old data' + new_data = b'new data' + sdk.mutate(old_data=old_data, new_data=new_data) + sdk.close() + + + def test_sdk_should_transit(self) -> None: + sdk_info: SdkInfo = SdkInfo.from_json(json.dumps(self.test_json)) + annotator_factory = AnnotatorFactory() + annotators = [annotator_factory.get_annotator(kind=annotation_type, sdk_info=sdk_info) for annotation_type in sdk_info.annotators] + + logger = logging.getLogger(__name__) + logging.basicConfig(level = logging.DEBUG) + sdk = DefaultSdk(annotators=annotators, config=sdk_info, logger=logger) + + test_data = b'test' + sdk.transit(data=test_data) + sdk.close() + + def test_sdk_should_create_published_annotations(self) -> None: + sdk_info: SdkInfo = SdkInfo.from_json(json.dumps(self.test_json)) + annotator_factory = AnnotatorFactory() + annotators = [annotator_factory.get_annotator(kind=annotation_type, sdk_info=sdk_info) for annotation_type in sdk_info.annotators] + + logger = logging.getLogger(__name__) + logging.basicConfig(level = logging.DEBUG) + + sdk = DefaultSdk(annotators=annotators, config=sdk_info, logger=logger) + + test_data = b'test' + sdk.publish(data=test_data) + sdk.close() + +if __name__ == "__main__": + unittest.main() \ No newline at end of file