diff --git a/Pipfile b/Pipfile index ca31c4b80..a7dfb4ed1 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,7 @@ cryptography = ">=2.3" pysha3="*" requests = "*" sqlalchemy = "*" +marshmallow = "*" maya = "*" flask = "*" flask_sqlalchemy = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 59027f263..58f985ef4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2a36b4160cdf1d70dbdb8ae0113c35e7e2f4cf97bff4088c9ca0133c45f592b1" + "sha256": "da7b7364c172c115fa1d88b6e6e35128dfde996366b51b08e2c3b1eacff52387" }, "pipfile-spec": 6, "requires": { @@ -483,6 +483,14 @@ ], "version": "==1.1.1" }, + "marshmallow": { + "hashes": [ + "sha256:0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e", + "sha256:3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f" + ], + "index": "pypi", + "version": "==3.3.0" + }, "maya": { "hashes": [ "sha256:7f53e06d5a123613dce7c270cbc647643a6942590dba7a19ec36194d0338c3f4", diff --git a/dev-requirements.txt b/dev-requirements.txt index 505760a44..e69de29bb 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,59 +0,0 @@ --i https://pypi.python.org/simple -aafigure==0.6 -alabaster==0.7.12 -ansible==2.9.2 -apipkg==1.5 -attrs==19.3.0 -babel==2.7.0 -bumpversion==0.5.3 -certifi==2019.11.28 -cffi==1.13.2 -chardet==3.0.4 -commonmark==0.9.1 -coverage==5.0.1 -cryptography==2.8 -decorator==4.4.1 -docutils==0.16rc1 -execnet==1.7.1 -git+https://github.com/nucypher/py-solc.git@391b8da1a6bac5816877197bda25527c6b0b8c15#egg=py-solc -greenlet==0.4.15 -idna==2.8 -imagesize==1.1.0 -importlib-metadata==1.3.0 ; python_version < '3.8' -jinja2==2.10.3 -markupsafe==1.1.1 -more-itertools==8.0.2 -mypy-extensions==0.4.3 -mypy==0.761 -packaging==19.2 -pluggy==0.13.1 -py==1.8.0 -pycparser==2.19 -pygments==2.5.2 -pyparsing==2.4.5 -pytest-cov==2.8.1 -pytest-forked==1.1.3 -pytest-mock==1.13.0 -pytest-mypy==0.4.2 -pytest-twisted==1.12 -pytest-xdist==1.31.0 -pytest==5.3.2 -pytz==2019.3 -pyyaml==5.3b1 -recommonmark==0.6.0 -requests==2.22.0 -six==1.13.0 -snowballstemmer==2.0.0 -sphinx-rtd-theme==0.4.3 -sphinx==2.3.1 -sphinxcontrib-applehelp==1.0.1 -sphinxcontrib-devhelp==1.0.1 -sphinxcontrib-htmlhelp==1.0.2 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.2 -sphinxcontrib-serializinghtml==1.1.3 -typed-ast==1.4.0 -typing-extensions==3.7.4.1 -urllib3==1.25.7 -wcwidth==0.1.7 -zipp==0.6.0 diff --git a/nucypher/characters/control/specifications.py b/nucypher/characters/control/specifications/__init__.py similarity index 59% rename from nucypher/characters/control/specifications.py rename to nucypher/characters/control/specifications/__init__.py index 19b1f217c..c9a627069 100644 --- a/nucypher/characters/control/specifications.py +++ b/nucypher/characters/control/specifications/__init__.py @@ -1,3 +1,8 @@ + +from .bob import specifications as bob +from .alice import specifications as alice +from .enrico import specifications as enrico + from abc import ABC from collections import namedtuple @@ -33,6 +38,32 @@ class CharacterSpecification(ABC): k: spec.get(k, ()) for k in ['input', 'optional', 'output']}) + @classmethod + def get_spec(cls, interface_name: str) -> tuple: + if cls._specifications is NotImplemented: + raise NotImplementedError("Missing specifications for character") + try: + spec = cls.specifications()[interface_name] + except KeyError: + raise cls.SpecificationError(f"{cls.__class__.__name__} has no such control interface: '{interface_name}'") + + return spec + + @classmethod + def get_specifications(cls, interface_name: str) -> tuple: + spec = cls.get_spec(interface_name) + + if isinstance(spec, dict): + return SpecificationTuple(**{ + k: spec.get(k, ()) + for k in ['input', 'optional', 'output']}) + + return SpecificationTuple( + [k for k, f in spec.load_fields.items() if f.required], + [k for k, f in spec.load_fields.items() if not f.required], + list(spec.dump_fields.keys()) + ) + @classmethod def specifications(cls): if cls._specifications is NotImplemented: @@ -65,56 +96,14 @@ class CharacterSpecification(ABC): _, _, output_specification = self.get_specifications(interface_name=interface_name) return self.__validate(specification=output_specification, data=response, error_class=self.InvalidInputField) - class AliceSpecification(CharacterSpecification): - __create_policy = {'input': ('bob_encrypting_key', 'bob_verifying_key', 'm', 'n', 'label', 'expiration'), - 'optional': ('value', 'rate'), - 'output': ('label', 'policy_encrypting_key')} - - __derive_policy_encrypting_key = {'input': ('label', ), - 'output': ('policy_encrypting_key', 'label')} - - __grant = {'input': ('bob_encrypting_key', 'bob_verifying_key', 'm', 'n', 'label', 'expiration'), - 'optional': ('value', 'rate'), - 'output': ('treasure_map', 'policy_encrypting_key', 'alice_verifying_key')} - - __revoke = {'input': ('label', 'bob_verifying_key', ), - 'output': ('failed_revocations',)} - - __decrypt = {'input': ('label', 'message_kit'), - 'output': ('cleartexts', )} - - __public_keys = {'input': (), - 'output': ('alice_verifying_key',)} - - _specifications = {'create_policy': __create_policy, # type: Tuple[Tuple[str]] - 'derive_policy_encrypting_key': __derive_policy_encrypting_key, - 'grant': __grant, - 'revoke': __revoke, - 'public_keys': __public_keys, - 'decrypt': __decrypt, } - + _specifications = alice class BobSpecification(CharacterSpecification): - __join_policy = {'input': ('label', 'alice_verifying_key'), - 'output': ('policy_encrypting_key', )} - - __retrieve = {'input': ('label', 'policy_encrypting_key', 'alice_verifying_key', 'message_kit'), - 'output': ('cleartexts', )} - - __public_keys = {'input': (), - 'output': ('bob_encrypting_key', 'bob_verifying_key')} - - _specifications = {'join_policy': __join_policy, - 'retrieve': __retrieve, - 'public_keys': __public_keys} - + _specifications = bob class EnricoSpecification(CharacterSpecification): - __encrypt_message = {'input': ('message', ), - 'output': ('message_kit', 'signature')} - - _specifications = {'encrypt_message': __encrypt_message} + _specifications = enrico \ No newline at end of file diff --git a/nucypher/characters/control/specifications/alice.py b/nucypher/characters/control/specifications/alice.py new file mode 100644 index 000000000..0bf390668 --- /dev/null +++ b/nucypher/characters/control/specifications/alice.py @@ -0,0 +1,68 @@ +from marshmallow import Schema +from .fields import fields + + +class PolicyBaseSchema(Schema): + + #required input fields + bob_encrypting_key = fields.Key(required=True, load_only=True) + bob_verifying_key = fields.Key(required=True, load_only=True) + m = fields.Integer(required=True, load_only=True) + n = fields.Integer(required=True, load_only=True) + expiration = fields.Date(required=True, load_only=True) + + # optional input + value = fields.Integer(load_only=True) + first_period_reward = fields.Integer(load_only=True) + rate = fields.Integer(load_only=True) + + #output + policy_encrypting_key = fields.Key(dump_only=True) + + +class CreatePolicy(PolicyBaseSchema): + + label = fields.Str(required=True) + + +class GrantPolicy(PolicyBaseSchema): + + treasure_map = fields.TreasureMap(dump_only=True) + alice_verifying_key = fields.Key(dump_only=True) + label = fields.Str(load_only=True, required=True) + + +class DerivePolicyEncryptionKey(Schema): + + label = fields.Str(required=True) + policy_encrypting_key = fields.Key(dump_only=True) + + +class Revoke(Schema): + + label = fields.Str(required=True, load_only=True) + bob_verifying_key = fields.Key(required=True, load_only=True) + + failed_revocations = fields.List(fields.Str(), dump_only=True) + + +class Decrypt(Schema): + label = fields.Str(required=True, load_only=True) + message_kit = fields.MessageKit(load_only=True) + + cleartexts = fields.List(fields.Str(), dump_only=True) + + +class PublicKeys(Schema): + + alice_verifying_key = fields.Key(dump_only=True) + + +specifications = { + 'create_policy': CreatePolicy(), + 'derive_policy_encrypting_key': DerivePolicyEncryptionKey(), + 'grant': GrantPolicy(), + 'revoke': Revoke(), + 'public_keys': PublicKeys(), + 'decrypt': Decrypt(), + } \ No newline at end of file diff --git a/nucypher/characters/control/specifications/bob.py b/nucypher/characters/control/specifications/bob.py new file mode 100644 index 000000000..adee13df5 --- /dev/null +++ b/nucypher/characters/control/specifications/bob.py @@ -0,0 +1,13 @@ + +__join_policy = {'input': ('label', 'alice_verifying_key'), + 'output': ('policy_encrypting_key', )} + +__retrieve = {'input': ('label', 'policy_encrypting_key', 'alice_verifying_key', 'message_kit'), + 'output': ('cleartexts', )} + +__public_keys = {'input': (), + 'output': ('bob_encrypting_key', 'bob_verifying_key')} + +specifications = {'join_policy': __join_policy, + 'retrieve': __retrieve, + 'public_keys': __public_keys} \ No newline at end of file diff --git a/nucypher/characters/control/specifications/enrico.py b/nucypher/characters/control/specifications/enrico.py new file mode 100644 index 000000000..ee9ec3332 --- /dev/null +++ b/nucypher/characters/control/specifications/enrico.py @@ -0,0 +1,5 @@ + +__encrypt_message = {'input': ('message', ), + 'output': ('message_kit', 'signature')} + +specifications = {'encrypt_message': __encrypt_message} diff --git a/nucypher/characters/control/specifications/fields/__init__.py b/nucypher/characters/control/specifications/fields/__init__.py new file mode 100644 index 000000000..10bde3533 --- /dev/null +++ b/nucypher/characters/control/specifications/fields/__init__.py @@ -0,0 +1,7 @@ +from marshmallow import fields +from .key import * +from .treasuremap import * +from .messagekit import * + + + diff --git a/nucypher/characters/control/specifications/fields/key.py b/nucypher/characters/control/specifications/fields/key.py new file mode 100644 index 000000000..8128d34b7 --- /dev/null +++ b/nucypher/characters/control/specifications/fields/key.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +class KeyField(fields.Field): + + def _serialize(self, value, attr, obj, **kwargs): + + return value + + def _deserialize(self, value, attr, data, **kwargs): + return value + +fields.Key = KeyField \ No newline at end of file diff --git a/nucypher/characters/control/specifications/fields/messagekit.py b/nucypher/characters/control/specifications/fields/messagekit.py new file mode 100644 index 000000000..0eb6fe0df --- /dev/null +++ b/nucypher/characters/control/specifications/fields/messagekit.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +class MessageKitField(fields.Field): + + def _serialize(self, value, attr, obj, **kwargs): + + return value + + def _deserialize(self, value, attr, data, **kwargs): + return value + +fields.MessageKit = MessageKitField \ No newline at end of file diff --git a/nucypher/characters/control/specifications/fields/treasuremap.py b/nucypher/characters/control/specifications/fields/treasuremap.py new file mode 100644 index 000000000..5bb43d9af --- /dev/null +++ b/nucypher/characters/control/specifications/fields/treasuremap.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +class TreasureMapField(fields.Field): + + def _serialize(self, value, attr, obj, **kwargs): + + return value + + def _deserialize(self, value, attr, data, **kwargs): + return value + +fields.TreasureMap = TreasureMapField \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2023eca76..e69de29bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,102 +0,0 @@ --i https://pypi.python.org/simple -appdirs==1.4.3 -argh==0.26.2 -asn1crypto==1.2.0 -attrdict==2.0.1 -attrs==19.3.0 -autobahn==19.11.1 -automat==0.8.0 -base58==1.0.3 -blake2b-py==0.1.3 -bytestring-splitter==1.3.0 -cached-property==1.5.1 -certifi==2019.11.28 -cffi==1.13.2 -chardet==3.0.4 -click==7.0 -coincurve==13.0.0 -colorama==0.4.3 -constant-sorrow==0.1.0a8 -constantly==15.1.0 -cryptography==2.8 -cytoolz==0.10.1 ; implementation_name == 'cpython' -dateparser==0.7.2 -eth-abi==2.1.0 -eth-account==0.4.0 -eth-bloom==1.0.3 -eth-hash[pycryptodome]==0.2.0 -eth-keyfile==0.5.1 -eth-keys==0.2.4 -eth-rlp==0.1.2 -eth-tester==0.4.0b1 -eth-typing==2.2.1 -eth-utils==1.8.4 -flask-limiter==1.1.0 -flask-sqlalchemy==2.4.1 -flask==1.1.1 -hendrix==3.2.5 -hexbytes==0.2.0 -humanize==0.5.1 -hyperlink==19.0.0 -idna==2.8 -importlib-metadata==1.3.0 ; python_version < '3.8' -incremental==17.5.0 -ipfshttpclient==0.4.12 -itsdangerous==1.1.0 -jinja2==2.10.3 -jsonschema==3.2.0 -limits==1.4.1 -lru-dict==1.1.6 -markupsafe==1.1.1 -maya==0.6.1 -more-itertools==8.0.2 -msgpack-python==0.5.6 -multiaddr==0.0.9 -mypy-extensions==0.4.3 -netaddr==0.7.19 -parsimonious==0.8.1 -pathtools==0.1.2 -pendulum==2.0.5 -protobuf==3.11.2 -py-ecc==1.7.1 -py-evm==0.3.0a12 -py-geth==2.2.0 -pyasn1-modules==0.2.7 -pyasn1==0.4.8 -pychalk==2.0.1 -pycparser==2.19 -pycryptodome==3.9.4 -pyethash==0.1.27 -pyhamcrest==1.9.0 -pynacl==1.3.0 -pyopenssl==19.1.0 -pyrsistent==0.15.6 -pysha3==1.0.2 -python-dateutil==2.8.1 -pytz==2019.3 -pytzdata==2019.3 -pyyaml==5.3b1 -regex==2019.12.20 -requests==2.22.0 -rlp==1.2.0 -semantic-version==2.8.4 -sentry-sdk==0.9.0 -service-identity==18.1.0 -six==1.13.0 -snaptime==0.2.4 -sqlalchemy==1.3.12 -toolz==0.10.0 -trie==1.4.0 -twisted==19.10.0 -txaio==18.8.1 -typing-extensions==3.7.4.1 -tzlocal==2.0.0 -umbral==0.1.3a2 -urllib3==1.25.7 -varint==1.0.2 -watchdog==0.9.0 -web3==5.4.0 -websockets==8.1 -werkzeug==0.16.0 -zipp==0.6.0 -zope.interface==4.7.1 diff --git a/tests/characters/test_specifications.py b/tests/characters/test_specifications.py new file mode 100644 index 000000000..61891afba --- /dev/null +++ b/tests/characters/test_specifications.py @@ -0,0 +1,13 @@ +import pytest +from marshmallow.exceptions import ValidationError +from nucypher.characters.control.specifications import AliceSpecification + + +def test_alice_spec_validate(federated_alice, federated_bob): + + createPolicy = AliceSpecification.get_spec('create_policy') + specs = AliceSpecification.get_specifications('create_policy') + + with pytest.raises(ValidationError) as e: + result = createPolicy.load({}) +