diff --git a/nucypher/acumen/perception.py b/nucypher/acumen/perception.py index 6b14bf7da..84555e21e 100644 --- a/nucypher/acumen/perception.py +++ b/nucypher/acumen/perception.py @@ -22,14 +22,11 @@ from collections import deque from collections.abc import KeysView from typing import Optional, Dict, Iterable, List, Tuple, NamedTuple, Union, Any -import binascii -import itertools import maya from eth_typing import ChecksumAddress from nucypher_core import FleetStateChecksum, NodeMetadata -from ..crypto.utils import keccak_digest from nucypher.utilities.logging import Logger from .nicknames import Nickname diff --git a/nucypher/characters/control/specifications/fields/__init__.py b/nucypher/characters/control/specifications/fields/__init__.py index 17912d924..ae5e73bab 100644 --- a/nucypher/characters/control/specifications/fields/__init__.py +++ b/nucypher/characters/control/specifications/fields/__init__.py @@ -23,4 +23,3 @@ from nucypher.characters.control.specifications.fields.label import * from nucypher.characters.control.specifications.fields.cleartext import * from nucypher.characters.control.specifications.fields.misc import * from nucypher.characters.control.specifications.fields.file import * -from nucypher.characters.control.specifications.fields.signature import * diff --git a/nucypher/characters/control/specifications/fields/signature.py b/nucypher/characters/control/specifications/fields/signature.py deleted file mode 100644 index cda78a710..000000000 --- a/nucypher/characters/control/specifications/fields/signature.py +++ /dev/null @@ -1,44 +0,0 @@ -""" - This file is part of nucypher. - - nucypher is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - nucypher is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with nucypher. If not, see . -""" - -from base64 import b64decode, b64encode - -from marshmallow import fields - -from nucypher.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes -from nucypher.control.specifications.fields.base import BaseField -from nucypher.crypto.umbral_adapter import Signature - - -class UmbralSignature(BaseField, fields.Field): - - def _serialize(self, value: Signature, attr, obj, **kwargs): - return b64encode(bytes(value)).decode() - - def _deserialize(self, value, attr, data, **kwargs): - if isinstance(value, bytes): - return value - try: - return Signature.from_bytes(b64decode(value)) - except InvalidNativeDataTypes as e: - raise InvalidInputData(f"Could not parse {self.name}: {e}") - - def _validate(self, value): - try: - Signature.from_bytes(value) - except InvalidNativeDataTypes as e: - raise InvalidInputData(f"Could not parse {self.name}: {e}") diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index f978bd0bb..f8b98d3cf 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -21,7 +21,6 @@ from http import HTTPStatus import json import time from base64 import b64encode -from datetime import datetime from json.decoder import JSONDecodeError from pathlib import Path from queue import Queue @@ -39,11 +38,9 @@ from constant_sorrow.constants import ( from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509 import Certificate, NameOID from eth_typing.evm import ChecksumAddress -from eth_utils import to_canonical_address, to_checksum_address from flask import Response, request from twisted.internet import reactor, stdio from twisted.internet.defer import Deferred -from twisted.internet.task import LoopingCall from twisted.logger import Logger from web3.types import TxReceipt @@ -88,7 +85,6 @@ from nucypher.crypto.umbral_adapter import ( reencrypt, VerifiedKeyFrag, ) -from nucypher.datastore.datastore import DatastoreTransactionError, RecordNotFound from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher @@ -360,7 +356,7 @@ class Alice(Character, BlockchainPolicyAuthor): return policy_pubkey def revoke(self, - policy: 'Policy', + policy: Policy, onchain: bool = True, # forced to False for federated mode offchain: bool = True ) -> Tuple[TxReceipt, Dict[ChecksumAddress, Tuple['Revocation', Exception]]]: @@ -766,8 +762,7 @@ class Ursula(Teacher, Character, Worker): self.rest_server = self._make_local_server(host=rest_host, port=rest_port, - db_filepath=db_filepath, - domain=domain) + db_filepath=db_filepath) # Self-signed TLS certificate of self for Teacher.__init__ certificate_filepath = self._crypto_power.power_ups(TLSHostingPower).keypair.certificate_filepath @@ -809,11 +804,10 @@ class Ursula(Teacher, Character, Worker): self._crypto_power.consume_power_up(tls_hosting_power) # Consume! return tls_hosting_power - def _make_local_server(self, host, port, domain, db_filepath) -> ProxyRESTServer: + def _make_local_server(self, host, port, db_filepath) -> ProxyRESTServer: rest_app, datastore = make_rest_app( this_node=self, db_filepath=db_filepath, - domain=domain, ) rest_server = ProxyRESTServer(rest_host=host, rest_port=port, diff --git a/nucypher/core.py b/nucypher/core.py deleted file mode 100644 index 36ddf3b8b..000000000 --- a/nucypher/core.py +++ /dev/null @@ -1,1025 +0,0 @@ -""" -This file is part of nucypher. - -nucypher is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -nucypher is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with nucypher. If not, see . -""" - -from typing import Optional, Sequence, Dict, Tuple, List, Iterable, Mapping, NamedTuple, Callable - -from bytestring_splitter import ( - BytestringSplitter, - VariableLengthBytestring, - BytestringKwargifier, - BytestringSplittingError, -) -from eth_typing.evm import ChecksumAddress -from eth_utils.address import to_checksum_address, to_canonical_address - -from nucypher.utilities.versioning import Versioned - -from nucypher.blockchain.eth.constants import LENGTH_ECDSA_SIGNATURE_WITH_RECOVERY -from nucypher.crypto.utils import keccak_digest -from nucypher.crypto.signing import InvalidSignature -import nucypher.crypto.umbral_adapter as umbral # need it to mock `umbral.encrypt` -from nucypher.crypto.umbral_adapter import ( - SecretKey, - PublicKey, - Signer, - Capsule, - Signature, - CapsuleFrag, - VerifiedCapsuleFrag, - KeyFrag, - VerifiedKeyFrag, - VerificationError, - decrypt_original, - decrypt_reencrypted, - ) - - -ETH_ADDRESS_BYTE_LENGTH = 20 - -key_splitter = BytestringSplitter((PublicKey, PublicKey.serialized_size())) -signature_splitter = BytestringSplitter((Signature, Signature.serialized_size())) -capsule_splitter = BytestringSplitter((Capsule, Capsule.serialized_size())) -cfrag_splitter = BytestringSplitter((CapsuleFrag, CapsuleFrag.serialized_size())) -kfrag_splitter = BytestringSplitter((KeyFrag, KeyFrag.serialized_size())) -checksum_address_splitter = BytestringSplitter((to_checksum_address, ETH_ADDRESS_BYTE_LENGTH)) # TODO: is there a pre-defined constant? -variable_length_splitter = BytestringSplitter(VariableLengthBytestring) - - -_OPTIONAL_NONE = b'\x00' -_OPTIONAL_SOME = b'\x01' - - -def serialize_optional(value: Optional) -> bytes: - if value is None: - return _OPTIONAL_NONE - else: - return _OPTIONAL_SOME + bytes(value) - - -def take_optional(take_obj: Callable[[bytes], Tuple[object, bytes]], data: bytes): - optional_flag = data[0:1] - remainder = data[1:] - - if optional_flag == _OPTIONAL_NONE: - return None, remainder - elif optional_flag == _OPTIONAL_SOME: - obj, remainder = take_obj(remainder) - return obj, remainder - else: - raise ValueError(f"Incorrect optional flag: {optional_flag}") - - -def take_decentralized_identity_evidence(data): - expected_length = LENGTH_ECDSA_SIGNATURE_WITH_RECOVERY - if len(data) < LENGTH_ECDSA_SIGNATURE_WITH_RECOVERY: - raise ValueError(f"Not enough bytes to fit a decentralized_identity_evidence ({len(data)})") - return data[:expected_length], data[expected_length:] - - -class MessageKit(Versioned): - """ - A message encrypted for re-encryption - """ - - @classmethod - def author(cls, policy_encrypting_key: PublicKey, plaintext: bytes) -> 'MessageKit': - capsule, ciphertext = umbral.encrypt(policy_encrypting_key, plaintext) - return cls(capsule=capsule, ciphertext=ciphertext) - - def __init__(self, capsule: Capsule, ciphertext: bytes): - self.ciphertext = ciphertext - self.capsule = capsule - - def __eq__(self, other): - return (self.ciphertext == other.ciphertext and - self.capsule == other.capsule) - - def decrypt(self, sk: SecretKey) -> bytes: - return decrypt_original(sk, self.capsule, self.ciphertext) - - def decrypt_reencrypted(self, - sk: SecretKey, - policy_encrypting_key: PublicKey, - cfrags: Sequence[VerifiedCapsuleFrag], - ) -> bytes: - return decrypt_reencrypted(sk, policy_encrypting_key, self.capsule, cfrags, self.ciphertext) - - def __str__(self): - return f"{self.__class__.__name__}({self.capsule})" - - def _payload(self) -> bytes: - return bytes(self.capsule) + VariableLengthBytestring(self.ciphertext) - - @classmethod - def _brand(cls) -> bytes: - return b'MKit' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - @classmethod - def _from_bytes_current(cls, data): - splitter = BytestringSplitter(capsule_splitter, VariableLengthBytestring) - capsule, ciphertext, remainder = splitter(data, return_remainder=True) - return cls(capsule, ciphertext), remainder - - -class HRAC: - """ - "hashed resource access code". - - A hash of: - * Publisher's verifying key - * Bob's verifying key - * the label - - Publisher and Bob have all the information they need to construct this. - Ursula does not, so we share it with her. - - This way, Bob can generate it and use it to find the TreasureMap. - """ - - # Note: this corresponds to the hardcoded size in the contracts - # (which use `byte16` for this variable). - SIZE = 16 - - @classmethod - def derive(cls, publisher_verifying_key: PublicKey, bob_verifying_key: PublicKey, label: bytes) -> 'HRAC': - return cls(keccak_digest(bytes(publisher_verifying_key) + bytes(bob_verifying_key) + label)[:cls.SIZE]) - - def __init__(self, hrac_bytes: bytes): - self._hrac_bytes = hrac_bytes - - def __bytes__(self): - return self._hrac_bytes - - @classmethod - def from_bytes(cls, data: bytes) -> 'HRAC': - if len(data) != cls.SIZE: - raise ValueError(f"Incorrect HRAC size: expected {cls.SIZE}, got {len(data)}") - return cls(data) - - def __eq__(self, other): - return self._hrac_bytes == other._hrac_bytes - - def __hash__(self): - return hash(self._hrac_bytes) - - def __str__(self): - return f"HRAC({self._hrac_bytes.hex()})" - - -hrac_splitter = BytestringSplitter((HRAC, HRAC.SIZE)) - - -class AuthorizedKeyFrag(Versioned): - - def __init__(self, signature: Signature, kfrag: KeyFrag): - self.signature = signature - self.kfrag = kfrag - - @classmethod - def construct_by_publisher(cls, - signer: Signer, - hrac: HRAC, - verified_kfrag: VerifiedKeyFrag, - ) -> 'AuthorizedKeyFrag': - - # "un-verify" kfrag to keep further logic streamlined - kfrag = KeyFrag.from_bytes(bytes(verified_kfrag)) - - # Publisher makes plain to Ursula that, upon decrypting this message, - # this particular KFrag is authorized for use in the policy identified by this HRAC. - signature = signer.sign(bytes(hrac) + bytes(kfrag)) - - return cls(signature, kfrag) - - def _payload(self) -> bytes: - """Returns the unversioned bytes serialized representation of this instance.""" - return bytes(self.signature) + bytes(self.kfrag) - - @classmethod - def _brand(cls) -> bytes: - return b'AKFr' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - @classmethod - def _from_bytes_current(cls, data): - splitter = BytestringSplitter(signature_splitter, kfrag_splitter) - signature, kfrag, remainder = splitter(data, return_remainder=True) - return cls(signature, kfrag), remainder - - def verify(self, - hrac: HRAC, - publisher_verifying_key: PublicKey, - ) -> VerifiedKeyFrag: - - signed_message = bytes(hrac) + bytes(self.kfrag) - - if not self.signature.verify(message=signed_message, verifying_pk=publisher_verifying_key): - raise InvalidSignature("HRAC + KeyFrag are not signed by the provided publisher") - - # Ursula has no side channel to get the KeyFrag author's key, - # so verifying the keyfrag is useless. - # TODO: assuming here that VerifiedKeyFrag and KeyFrag have the same byte representation; - # would it be more clear if `kfrag` had some method like `force_verify()`? - verified_kfrag = VerifiedKeyFrag.from_verified_bytes(bytes(self.kfrag)) - - return verified_kfrag - - -class EncryptedKeyFrag: - - _splitter = BytestringSplitter(capsule_splitter, VariableLengthBytestring) - - @classmethod - def author(cls, recipient_key: PublicKey, authorized_kfrag: AuthorizedKeyFrag): - # TODO: using Umbral for encryption to avoid introducing more crypto primitives. - # Most probably it is an overkill, unless it can be used somehow - # for Ursula-to-Ursula "baton passing". - capsule, ciphertext = umbral.encrypt(recipient_key, bytes(authorized_kfrag)) - return cls(capsule, ciphertext) - - def __init__(self, capsule: Capsule, ciphertext: bytes): - self.capsule = capsule - self.ciphertext = ciphertext - - def decrypt(self, sk: SecretKey) -> AuthorizedKeyFrag: - cleartext = decrypt_original(sk, self.capsule, self.ciphertext) - return AuthorizedKeyFrag.from_bytes(cleartext) - - def __bytes__(self): - return bytes(self.capsule) + bytes(VariableLengthBytestring(self.ciphertext)) - - # Ideally we would define a splitter here that would deserialize into an EKF, - # but due to BSS limitations it cannot be nested (since it doesn't have a definite size). - # So we have to define this helper method and use that instead. - @classmethod - def take(cls, data): - capsule, ciphertext, remainder = cls._splitter(data, return_remainder=True) - return cls(capsule, ciphertext), remainder - - def __eq__(self, other): - return self.capsule == other.capsule and self.ciphertext == other.ciphertext - - -class TreasureMap(Versioned): - - def __init__(self, - threshold: int, - hrac: HRAC, - policy_encrypting_key: PublicKey, - publisher_verifying_key: PublicKey, - destinations: Dict[ChecksumAddress, EncryptedKeyFrag]): - self.threshold = threshold - self.destinations = destinations - self.hrac = hrac - self.policy_encrypting_key = policy_encrypting_key - self.publisher_verifying_key = publisher_verifying_key - - def __iter__(self): - return iter(self.destinations.items()) - - def __len__(self): - return len(self.destinations) - - def __eq__(self, other): - if not isinstance(other, TreasureMap): - return False - - return (self.threshold == other.threshold and - self.hrac == other.hrac and - self.destinations == other.destinations) - - @classmethod - def construct_by_publisher(cls, - signer: Signer, - hrac: HRAC, - policy_encrypting_key: PublicKey, - assigned_kfrags: Mapping[ChecksumAddress, Tuple[PublicKey, VerifiedKeyFrag]], - threshold: int, - ) -> 'TreasureMap': - """Create a new treasure map for a collection of ursulas and kfrags.""" - - if threshold < 1 or threshold > 255: - raise ValueError("The threshold must be between 1 and 255.") - - if len(assigned_kfrags) < threshold: - raise ValueError( - f"The number of destinations ({len(assigned_kfrags)}) " - f"must be equal or greater than the threshold ({threshold})") - - # Encrypt each kfrag for an Ursula. - destinations = {} - for ursula_address, key_and_kfrag in assigned_kfrags.items(): - ursula_key, verified_kfrag = key_and_kfrag - authorized_kfrag = AuthorizedKeyFrag.construct_by_publisher(signer=signer, - hrac=hrac, - verified_kfrag=verified_kfrag, - ) - encrypted_kfrag = EncryptedKeyFrag.author(recipient_key=ursula_key, - authorized_kfrag=authorized_kfrag) - - destinations[ursula_address] = encrypted_kfrag - - return cls(threshold=threshold, - hrac=hrac, - policy_encrypting_key=policy_encrypting_key, - publisher_verifying_key=signer.verifying_key(), - destinations=destinations) - - @classmethod - def _brand(cls) -> bytes: - return b'TMap' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - def _payload(self) -> bytes: - """Returns the unversioned bytes serialized representation of this instance.""" - assigned_kfrags = b''.join( - to_canonical_address(ursula_address) + bytes(encrypted_kfrag) - for ursula_address, encrypted_kfrag in self.destinations.items() - ) - - return (self.threshold.to_bytes(1, "big") + - bytes(self.hrac) + - bytes(self.policy_encrypting_key) + - bytes(self.publisher_verifying_key) + - bytes(VariableLengthBytestring(assigned_kfrags))) - - @classmethod - def _from_bytes_current(cls, data): - - main_splitter = BytestringSplitter( - (int, 1, {'byteorder': 'big'}), - hrac_splitter, - key_splitter, - key_splitter, - VariableLengthBytestring, - ) - - threshold, hrac, policy_encrypting_key, publisher_verifying_key, assigned_kfrags_bytes, remainder = main_splitter(data, return_remainder=True) - - destinations = {} - while assigned_kfrags_bytes: - ursula_address, assigned_kfrags_bytes = checksum_address_splitter(assigned_kfrags_bytes, return_remainder=True) - ekf, assigned_kfrags_bytes = EncryptedKeyFrag.take(assigned_kfrags_bytes) - destinations[ursula_address] = ekf - - return cls(threshold, hrac, policy_encrypting_key, publisher_verifying_key, destinations), remainder - - def encrypt(self, - signer: Signer, - recipient_key: PublicKey, - ) -> 'EncryptedTreasureMap': - return EncryptedTreasureMap.construct_by_publisher(signer=signer, - recipient_key=recipient_key, - treasure_map=self) - - -class AuthorizedTreasureMap(Versioned): - - @classmethod - def construct_by_publisher(cls, - signer: Signer, - recipient_key: PublicKey, - treasure_map: TreasureMap - ) -> 'AuthorizedTreasureMap': - payload = bytes(recipient_key) + bytes(treasure_map) - signature = signer.sign(payload) - return cls(signature, treasure_map) - - def __init__(self, signature: Signature, treasure_map: TreasureMap): - self.signature = signature - self.treasure_map = treasure_map - - @classmethod - def _brand(cls) -> bytes: - return b'AMap' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - def _payload(self) -> bytes: - """Returns the unversioned bytes serialized representation of this instance.""" - return (bytes(self.signature) + - bytes(self.treasure_map)) - - @classmethod - def _from_bytes_current(cls, data): - signature, remainder = signature_splitter(data, return_remainder=True) - treasure_map, remainder = TreasureMap.take(remainder) - return cls(signature, treasure_map), remainder - - def verify(self, recipient_key: PublicKey, publisher_verifying_key: PublicKey) -> TreasureMap: - payload = bytes(recipient_key) + bytes(self.treasure_map) - if not self.signature.verify(message=payload, verifying_pk=publisher_verifying_key): - raise InvalidSignature("This TreasureMap does not contain the correct signature " - "from the publisher.") - return self.treasure_map - - -class EncryptedTreasureMap(Versioned): - - def __init__(self, capsule: Capsule, ciphertext: bytes): - self.capsule = capsule - self.ciphertext = ciphertext - - @classmethod - def construct_by_publisher(cls, - signer: Signer, - recipient_key: PublicKey, - treasure_map: TreasureMap, - ) -> 'EncryptedTreasureMap': - - # TODO: using Umbral for encryption to avoid introducing more crypto primitives. - # Most probably it is an overkill, unless it can be used somehow - # for Ursula-to-Ursula "baton passing". - - # TODO: `signer` here can be different from the one in TreasureMap, it seems. - # Do we ever cross-check them? Do we want to enforce them to be the same? - payload = AuthorizedTreasureMap.construct_by_publisher(signer=signer, - recipient_key=recipient_key, - treasure_map=treasure_map) - - capsule, ciphertext = umbral.encrypt(recipient_key, bytes(payload)) - return cls(capsule, ciphertext) - - def decrypt(self, sk: SecretKey) -> AuthorizedTreasureMap: - payload_bytes = decrypt_original(sk, self.capsule, self.ciphertext) - return AuthorizedTreasureMap.from_bytes(payload_bytes) - - def _payload(self) -> bytes: - return bytes(self.capsule) + bytes(VariableLengthBytestring(self.ciphertext)) - - @classmethod - def _brand(cls) -> bytes: - return b'EMap' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - @classmethod - def _from_bytes_current(cls, data): - splitter = BytestringSplitter(capsule_splitter, VariableLengthBytestring) - capsule, ciphertext, remainder = splitter(data, return_remainder=True) - return cls(capsule, ciphertext), remainder - - def __eq__(self, other): - return bytes(self) == bytes(other) - - def __hash__(self): - return hash((self.__class__, bytes(self))) - - -class ReencryptionRequest(Versioned): - """ - A request for an Ursula to reencrypt for several capsules. - """ - - @classmethod - def from_treasure_map(cls, - ursula_address: ChecksumAddress, - capsules: Sequence[Capsule], - treasure_map: TreasureMap, - bob_verifying_key: PublicKey, - ) -> 'ReencryptionRequest': - return cls(hrac=treasure_map.hrac, - publisher_verifying_key=treasure_map.publisher_verifying_key, - bob_verifying_key=bob_verifying_key, - encrypted_kfrag=treasure_map.destinations[ursula_address], - capsules=capsules, - ) - - def __init__(self, - hrac: HRAC, - publisher_verifying_key: PublicKey, - bob_verifying_key: PublicKey, - encrypted_kfrag: EncryptedKeyFrag, - capsules: List[Capsule]): - - self.hrac = hrac - self.publisher_verifying_key = publisher_verifying_key - self.bob_verifying_key = bob_verifying_key - self.encrypted_kfrag = encrypted_kfrag - self.capsules = capsules - - def _payload(self) -> bytes: - return (bytes(self.hrac) + - bytes(self.publisher_verifying_key) + - bytes(self.bob_verifying_key) + - bytes(self.encrypted_kfrag) + - bytes(VariableLengthBytestring(b''.join(bytes(capsule) for capsule in self.capsules))) - ) - - @classmethod - def _brand(cls) -> bytes: - return b'ReRq' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - @classmethod - def _from_bytes_current(cls, data): - splitter = (hrac_splitter + - key_splitter + - key_splitter) - - hrac, publisher_vk, bob_vk, remainder = splitter(data, return_remainder=True) - ekfrag, remainder = EncryptedKeyFrag.take(remainder) - capsule_bytes, remainder = variable_length_splitter(remainder, return_remainder=True) - capsules = capsule_splitter.repeat(capsule_bytes) - return cls(hrac, publisher_vk, bob_vk, ekfrag, capsules), remainder - - -class ReencryptionResponse(Versioned): - """ - A response from Ursula with reencrypted capsule frags. - """ - - @classmethod - def construct_by_ursula(cls, - signer: Signer, - capsules: List[Capsule], - cfrags: List[VerifiedCapsuleFrag], - ) -> 'ReencryptionResponse': - - # un-verify - cfrags = [CapsuleFrag.from_bytes(bytes(cfrag)) for cfrag in cfrags] - - capsules_bytes = b''.join(bytes(capsule) for capsule in capsules) - cfrags_bytes = b''.join(bytes(cfrag) for cfrag in cfrags) - signature = signer.sign(capsules_bytes + cfrags_bytes) - return cls(cfrags, signature) - - def __init__(self, cfrags: List[CapsuleFrag], signature: Signature): - self.cfrags = cfrags - self.signature = signature - - def _payload(self) -> bytes: - """Returns the unversioned bytes serialized representation of this instance.""" - return bytes(self.signature) + bytes(VariableLengthBytestring(b''.join(bytes(cfrag) for cfrag in self.cfrags))) - - @classmethod - def _brand(cls) -> bytes: - return b'ReRs' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - @classmethod - def _from_bytes_current(cls, data): - splitter = BytestringSplitter(signature_splitter, VariableLengthBytestring) - signature, cfrags_bytes, remainder = splitter(data, return_remainder=True) - - # We would never send a request with no capsules, so there should be cfrags. - # The splitter would fail anyway, this just makes the error message more clear. - if not cfrags_bytes: - raise ValueError(f"{cls.__name__} contains no cfrags") - - cfrags = cfrag_splitter.repeat(cfrags_bytes) - return cls(cfrags, signature), remainder - - def verify(self, - capsules: Sequence[Capsule], - alice_verifying_key: PublicKey, - ursula_verifying_key: PublicKey, - policy_encrypting_key: PublicKey, - bob_encrypting_key: PublicKey, - ) -> List[VerifiedCapsuleFrag]: - - if len(capsules) != len(self.cfrags): - raise ValueError("Mismatched number of capsules and cfrags") - - capsules_bytes = b''.join(bytes(capsule) for capsule in capsules) - cfrags_bytes = b''.join(bytes(cfrag) for cfrag in self.cfrags) - - # Validate re-encryption signature - if not self.signature.verify(ursula_verifying_key, capsules_bytes + cfrags_bytes): - message = (f"{capsules} and {self.cfrags} " - "are not properly signed by Ursula.") - raise InvalidSignature(message) - - verified_cfrags = {} - for capsule, cfrag in zip(capsules, self.cfrags): - verified_cfrags[capsule] = cfrag.verify(capsule, - verifying_pk=alice_verifying_key, - delegating_pk=policy_encrypting_key, - receiving_pk=bob_encrypting_key) - - return verified_cfrags - - -class RetrievalKit(Versioned): - """ - An object encapsulating the information necessary for retrieval of cfrags from Ursulas. - Contains the capsule and the checksum addresses of Ursulas from which the requester - already received cfrags. - """ - - @classmethod - def from_message_kit(cls, message_kit: MessageKit) -> 'RetrievalKit': - return cls(message_kit.capsule, set()) - - def __init__(self, capsule: Capsule, queried_addresses: Iterable[ChecksumAddress]): - self.capsule = capsule - # Can store cfrags too, if we're worried about Ursulas supplying duplicate ones. - self.queried_addresses = set(queried_addresses) - - def _payload(self) -> bytes: - return (bytes(self.capsule) + - bytes(VariableLengthBytestring(b''.join(to_canonical_address(address) for address in self.queried_addresses)))) - - @classmethod - def _brand(cls) -> bytes: - return b'RKit' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - @classmethod - def _from_bytes_current(cls, data): - splitter = BytestringSplitter(capsule_splitter, VariableLengthBytestring) - capsule, addresses_bytes, remainder = splitter(data, return_remainder=True) - if addresses_bytes: - addresses = checksum_address_splitter.repeat(addresses_bytes) - else: - addresses = () - return cls(capsule, addresses), remainder - - -class RevocationOrder(Versioned): - """ - Represents a string used by characters to perform a revocation on a specific Ursula. - """ - - @classmethod - def author(cls, - signer: Signer, - ursula_address: ChecksumAddress, - encrypted_kfrag: EncryptedKeyFrag, - ) -> 'RevocationOrder': - return cls(ursula_address=ursula_address, - encrypted_kfrag=encrypted_kfrag, - signature=signer.sign(cls._signed_payload(ursula_address, encrypted_kfrag))) - - def __init__(self, ursula_address: ChecksumAddress, encrypted_kfrag: EncryptedKeyFrag, signature: Signature): - self.ursula_address = ursula_address - self.encrypted_kfrag = encrypted_kfrag - self.signature = signature - - def __repr__(self): - return bytes(self) - - def __len__(self): - return len(bytes(self)) - - def __eq__(self, other): - return bytes(self) == bytes(other) - - @staticmethod - def _signed_payload(ursula_address, encrypted_kfrag): - return to_canonical_address(ursula_address) + bytes(encrypted_kfrag) - - def verify_signature(self, alice_verifying_key: PublicKey) -> bool: - """ - Verifies the revocation was from the provided pubkey. - """ - # TODO: raise an exception instead of returning `bool`? - payload = self._signed_payload(self.ursula_address, self.encrypted_kfrag) - if not self.signature.verify(payload, alice_verifying_key): - raise InvalidSignature(f"Revocation has an invalid signature: {self.signature}") - return True - - @classmethod - def _brand(cls) -> bytes: - return b'Revo' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - def _payload(self) -> bytes: - return bytes(self.signature) + self._signed_payload(self.ursula_address, self.encrypted_kfrag) - - @classmethod - def _from_bytes_current(cls, data): - splitter = BytestringSplitter( - signature_splitter, - checksum_address_splitter, # ursula canonical address - ) - signature, ursula_address, remainder = splitter(data, return_remainder=True) - ekfrag, remainder = EncryptedKeyFrag.take(remainder) - obj = cls(ursula_address=ursula_address, - encrypted_kfrag=ekfrag, - signature=signature) - return obj, remainder - - -class NodeMetadataPayload(NamedTuple): - - public_address: bytes - domain: str - timestamp_epoch: int - verifying_key: PublicKey - encrypting_key: PublicKey - certificate_bytes: bytes # serialized `cryptography.x509.Certificate` - host: str - port: int - decentralized_identity_evidence: Optional[bytes] # TODO: make its own type? - - _splitter = BytestringSplitter( - (bytes, ETH_ADDRESS_BYTE_LENGTH), # public_address - VariableLengthBytestring, # domain_bytes - (int, 4, {'byteorder': 'big'}), # timestamp_epoch - key_splitter, # verifying_key - key_splitter, # encrypting_key - VariableLengthBytestring, # certificate_bytes - VariableLengthBytestring, # host_bytes - (int, 2, {'byteorder': 'big'}), # port - ) - - def __bytes__(self): - as_bytes = bytes().join((self.public_address, - bytes(VariableLengthBytestring(self.domain.encode('utf-8'))), - self.timestamp_epoch.to_bytes(4, 'big'), - bytes(self.verifying_key), - bytes(self.encrypting_key), - bytes(VariableLengthBytestring(self.certificate_bytes)), - bytes(VariableLengthBytestring(self.host.encode('utf-8'))), - self.port.to_bytes(2, 'big'), - serialize_optional(self.decentralized_identity_evidence), - )) - return as_bytes - - @classmethod - def take(cls, data): - *fields, remainder = cls._splitter(data, return_remainder=True) - - (public_address, - domain, - timestamp_epoch, - verifying_key, - encrypting_key, - certificate_bytes, - host, - port, - ) = fields - - decentralized_identity_evidence, remainder = take_optional(take_decentralized_identity_evidence, remainder) - - obj = cls(public_address=public_address, - domain=domain.decode('utf-8'), - timestamp_epoch=timestamp_epoch, - verifying_key=verifying_key, - encrypting_key=encrypting_key, - certificate_bytes=certificate_bytes, - host=host.decode('utf-8'), - port=port, - decentralized_identity_evidence=decentralized_identity_evidence, - ) - - return obj, remainder - - @classmethod - def from_bytes(cls, data): - obj, remainder = cls.take(data) - if remainder: - raise ValueError(f"{len(remainder)} bytes remaining after deserializing {cls}") - return obj - - -class NodeMetadata(Versioned): - - @classmethod - def author(cls, signer: Signer, **kwds): - payload = NodeMetadataPayload(**kwds) - signature = signer.sign(bytes(payload)) - # TODO: we can cache payload bytes here, for later use in serialization/verification - return cls(signature=signature, payload=payload) - - def __init__(self, signature: Signature, payload: NodeMetadataPayload): - self.signature = signature - self._metadata_payload = payload - for name, value in payload._asdict().items(): - setattr(self, name, value) - - def verify(self) -> bool: - # Note: in order for this to make sense, `verifying_key` must be checked independently. - # Currently it is done in `validate_worker()` (using `decentralized_identity_evidence`) - # TODO: do this on deserialization? - return self.signature.verify(message=bytes(self._metadata_payload), verifying_pk=self.verifying_key) - - @classmethod - def _brand(cls) -> bytes: - return b'NdMd' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - def _payload(self): - return bytes(self.signature) + bytes(self._metadata_payload) - - @classmethod - def _from_bytes_current(cls, data: bytes): - signature, remainder = signature_splitter(data, return_remainder=True) - payload, remainder = NodeMetadataPayload.take(remainder) - return cls(signature=signature, payload=payload), remainder - - @classmethod - def _batch_from_bytes(cls, data: bytes): - nodes = [] - while data: - node, data = cls.take(data) - nodes.append(node) - return nodes - - -class MetadataRequest(Versioned): - - _fleet_state_checksum_splitter = BytestringSplitter((bytes, 32)) - - def __init__(self, - fleet_state_checksum: str, - announce_nodes: Optional[Iterable[NodeMetadata]] = None, - ): - - self.fleet_state_checksum = fleet_state_checksum - self.announce_nodes = announce_nodes - - @classmethod - def _brand(cls) -> bytes: - return b'MdRq' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - def _payload(self): - if self.announce_nodes: - nodes_bytes = b''.join(bytes(n) for n in self.announce_nodes) - else: - nodes_bytes = b'' - return bytes.fromhex(self.fleet_state_checksum) + bytes(VariableLengthBytestring(nodes_bytes)) - - @classmethod - def _from_bytes_current(cls, data): - splitter = BytestringSplitter( - (bytes, 32), # fleet state checksum - VariableLengthBytestring, - ) - fleet_state_checksum_bytes, nodes_bytes, remainder = splitter(data, return_remainder=True) - if nodes_bytes: - nodes = NodeMetadata._batch_from_bytes(nodes_bytes) - else: - nodes = None - obj = cls(fleet_state_checksum=fleet_state_checksum_bytes.hex(), - announce_nodes=nodes) - return obj, remainder - - -class MetadataResponse(Versioned): - - @classmethod - def author(cls, - signer: Signer, - timestamp_epoch: int, - this_node: Optional[NodeMetadata] = None, - other_nodes: Optional[Iterable[NodeMetadata]] = None, - ): - payload = cls._signed_payload(timestamp_epoch, this_node, other_nodes) - signature = signer.sign(payload) - return cls(signature=signature, - timestamp_epoch=timestamp_epoch, - this_node=this_node, - other_nodes=other_nodes) - - @staticmethod - def _signed_payload(timestamp_epoch, this_node, other_nodes): - timestamp = timestamp_epoch.to_bytes(4, byteorder="big") - nodes_payload = b''.join(bytes(node) for node in other_nodes) if other_nodes else b'' - return ( - timestamp + - serialize_optional(this_node) + - bytes(VariableLengthBytestring(nodes_payload)) - ) - - def __init__(self, - signature: Signature, - timestamp_epoch: int, - this_node: Optional[NodeMetadata] = None, - other_nodes: Optional[List[NodeMetadata]] = None, - ): - self.signature = signature - self.timestamp_epoch = timestamp_epoch - self.this_node = this_node - self.other_nodes = other_nodes - - def verify(self, verifying_pk: PublicKey): - payload = self._signed_payload(self.timestamp_epoch, self.this_node, self.other_nodes) - if not self.signature.verify(verifying_pk=verifying_pk, message=payload): - raise InvalidSignature("Incorrect payload signature for MetadataResponse") - - @classmethod - def _brand(cls) -> bytes: - return b'MdRs' - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 1, 0 - - @classmethod - def _old_version_handlers(cls) -> Dict: - return {} - - def _payload(self): - payload = self._signed_payload(self.timestamp_epoch, self.this_node, self.other_nodes) - return bytes(self.signature) + payload - - @classmethod - def _from_bytes_current(cls, data: bytes): - - splitter = BytestringSplitter( - signature_splitter, - (int, 4, {'byteorder': 'big'}), - ) - - signature, timestamp_epoch, remainder = splitter(data, return_remainder=True) - this_node, remainder = take_optional(NodeMetadata.take, remainder) - maybe_other_nodes, remainder = variable_length_splitter(remainder, return_remainder=True) - other_nodes = NodeMetadata._batch_from_bytes(maybe_other_nodes) if maybe_other_nodes else None - obj = cls(signature=signature, - timestamp_epoch=timestamp_epoch, - this_node=this_node, - other_nodes=other_nodes) - return obj, remainder diff --git a/nucypher/crypto/umbral_adapter.py b/nucypher/crypto/umbral_adapter.py index c69244dca..cdd639222 100644 --- a/nucypher/crypto/umbral_adapter.py +++ b/nucypher/crypto/umbral_adapter.py @@ -26,14 +26,10 @@ from nucypher_core.umbral import ( Signature, Signer, Capsule, - KeyFrag, VerifiedKeyFrag, CapsuleFrag, VerifiedCapsuleFrag, VerificationError, - encrypt, - decrypt_original, generate_kfrags, reencrypt, - decrypt_reencrypted, ) diff --git a/nucypher/network/retrieval.py b/nucypher/network/retrieval.py index 9508dcd89..2f5f1f8f5 100644 --- a/nucypher/network/retrieval.py +++ b/nucypher/network/retrieval.py @@ -20,7 +20,7 @@ import random from typing import Dict, Sequence, List from eth_typing.evm import ChecksumAddress -from eth_utils import to_checksum_address, to_canonical_address +from eth_utils import to_checksum_address from twisted.logger import Logger from nucypher_core import ( diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 794003366..638c3c9dc 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -23,7 +23,7 @@ from pathlib import Path from typing import Tuple from constant_sorrow import constants -from constant_sorrow.constants import RELAX, NOT_STAKING +from constant_sorrow.constants import RELAX from flask import Flask, Response, jsonify, request from mako import exceptions as mako_exceptions from mako.template import Template @@ -31,13 +31,11 @@ from mako.template import Template from nucypher_core import ( ReencryptionRequest, RevocationOrder, - NodeMetadata, MetadataRequest, MetadataResponse, MetadataResponsePayload, ) -from nucypher.blockchain.eth.utils import period_to_epoch from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH from nucypher.crypto.keypairs import DecryptingKeypair from nucypher.crypto.signing import InvalidSignature @@ -82,7 +80,6 @@ class ProxyRESTServer: def make_rest_app( db_filepath: Path, this_node, - domain, log: Logger = Logger("http-application-layer") ) -> Tuple[Flask, Datastore]: """ @@ -99,12 +96,12 @@ def make_rest_app( log.info("Starting datastore {}".format(db_filepath)) datastore = Datastore(db_filepath) - rest_app = _make_rest_app(weakref.proxy(datastore), weakref.proxy(this_node), domain, log) + rest_app = _make_rest_app(weakref.proxy(datastore), weakref.proxy(this_node), log) return rest_app, datastore -def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) -> Flask: +def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask: # TODO: Avoid circular imports :-( from nucypher.characters.lawful import Alice, Bob, Ursula diff --git a/nucypher/utilities/versioning.py b/nucypher/utilities/versioning.py deleted file mode 100644 index af65beaad..000000000 --- a/nucypher/utilities/versioning.py +++ /dev/null @@ -1,179 +0,0 @@ -""" - This file is part of nucypher. - - nucypher is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - nucypher is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with nucypher. If not, see . -""" - - -from abc import abstractmethod, ABC -import re -from typing import Dict, Tuple, Callable - - -class Versioned(ABC): - """Base class for serializable entities""" - - _VERSION_PARTS = 2 - _VERSION_PART_SIZE = 2 # bytes - _BRAND_SIZE = 4 - _VERSION_SIZE = _VERSION_PART_SIZE * _VERSION_PARTS - _HEADER_SIZE = _BRAND_SIZE + _VERSION_SIZE - - class InvalidHeader(ValueError): - """Raised when an unexpected or invalid bytes header is encountered.""" - - class IncompatibleVersion(ValueError): - """Raised when attempting to deserialize incompatible bytes""" - - class Empty(ValueError): - """Raised when 0 bytes are remaining after parsing the header.""" - - @classmethod - @abstractmethod - def _brand(cls) -> bytes: - raise NotImplementedError - - @classmethod - @abstractmethod - def _version(cls) -> Tuple[int, int]: - """tuple(major, minor)""" - raise NotImplementedError - - @classmethod - def version_string(cls) -> str: - major, minor = cls._version() - return f'{major}.{minor}' - - # - # Serialize - # - - def __bytes__(self) -> bytes: - return self._header() + self._payload() - - @classmethod - def _header(cls) -> bytes: - """The entire bytes header to prepend to the instance payload.""" - major, minor = cls._version() - major_bytes = major.to_bytes(cls._VERSION_PART_SIZE, 'big') - minor_bytes = minor.to_bytes(cls._VERSION_PART_SIZE, 'big') - header = cls._brand() + major_bytes + minor_bytes - return header - - @abstractmethod - def _payload(self) -> bytes: - """The unbranded and unversioned bytes-serialized representation of this instance.""" - raise NotImplementedError - - # - # Deserialize - # - - @classmethod - @abstractmethod - def _from_bytes_current(cls, data): - """The current deserializer""" - raise NotImplementedError - - @classmethod - @abstractmethod - def _old_version_handlers(cls) -> Dict[Tuple[int, int], Callable]: - """Old deserializer callables keyed by version.""" - raise NotImplementedError - - @classmethod - def take(cls, data: bytes): - """ - Deserializes the object from the given bytestring - and returns the object and the remainder of the bytestring. - """ - brand, version, payload = cls._parse_header(data) - version = cls._resolve_version(version=version) - handlers = cls._deserializers() - obj, remainder = handlers[version](payload) - return obj, remainder - - @classmethod - def from_bytes(cls, data: bytes): - """"Public deserialization API""" - obj, remainder = cls.take(data) - if remainder: - raise ValueError(f"{len(remainder)} bytes remaining after deserializing {cls}") - return obj - - @classmethod - def _resolve_version(cls, version: Tuple[int, int]) -> Tuple[int, int]: - - # Unpack version metadata - bytrestring_major, bytrestring_minor = version - latest_major_version, latest_minor_version = cls._version() - - # Enforce major version compatibility - if not bytrestring_major == latest_major_version: - message = f'Incompatible versioned bytes for {cls.__name__}. ' \ - f'Compatible version is {latest_major_version}.x, ' \ - f'Got {bytrestring_major}.{bytrestring_minor}.' - raise cls.IncompatibleVersion(message) - - # Enforce minor version compatibility. - # Pass future minor versions to the latest minor handler. - if bytrestring_minor >= latest_minor_version: - version = cls._version() - - return version - - @classmethod - def _parse_header(cls, data: bytes) -> Tuple[bytes, Tuple[int, int], bytes]: - if len(data) < cls._HEADER_SIZE: - # handles edge case when input is too short. - raise ValueError(f'Invalid bytes for {cls.__name__}.') - brand = cls._parse_brand(data) - version = cls._parse_version(data) - payload = cls._parse_payload(data) - return brand, version, payload - - @classmethod - def _parse_brand(cls, data: bytes) -> bytes: - brand = data[:cls._BRAND_SIZE] - if brand != cls._brand(): - error = f"Incorrect brand. Expected {cls._brand()}, Got {brand}." - if not re.fullmatch(rb'\w+', brand): - # unversioned entities for older versions will most likely land here. - error = f"Incompatible bytes for {cls.__name__}." - raise cls.InvalidHeader(error) - return brand - - @classmethod - def _parse_version(cls, data: bytes) -> Tuple[int, int]: - version_data = data[cls._BRAND_SIZE:cls._HEADER_SIZE] - major, minor = version_data[:cls._VERSION_PART_SIZE], version_data[cls._VERSION_PART_SIZE:] - major, minor = int.from_bytes(major, 'big'), int.from_bytes(minor, 'big') - version = major, minor - return version - - @classmethod - def _parse_payload(cls, data: bytes) -> bytes: - payload = data[cls._HEADER_SIZE:] - if len(payload) == 0: - raise ValueError(f'No content to deserialize {cls.__name__}.') - return payload - - @classmethod - def _deserializers(cls) -> Dict[Tuple[int, int], Callable]: - """Return a dict of all known deserialization handlers for this class keyed by version""" - return {cls._version(): cls._from_bytes_current, **cls._old_version_handlers()} - - -# Collects the brands of every serializable entity, potentially useful for documentation. -# SERIALIZABLE_ENTITIES = {v.__class__.__name__: v._brand() for v in Versioned.__subclasses__()} diff --git a/tests/contracts/lib/test_umbral_deserializer.py b/tests/contracts/lib/test_umbral_deserializer.py index b6a797abc..53f4b65a0 100644 --- a/tests/contracts/lib/test_umbral_deserializer.py +++ b/tests/contracts/lib/test_umbral_deserializer.py @@ -17,9 +17,13 @@ along with nucypher. If not, see . import os -import pytest + from eth_tester.exceptions import TransactionFailed -from nucypher.crypto.umbral_adapter import Signer, SecretKey, generate_kfrags, encrypt, reencrypt +import pytest + +from nucypher_core import MessageKit + +from nucypher.crypto.umbral_adapter import Signer, SecretKey, generate_kfrags, reencrypt @pytest.fixture() @@ -44,7 +48,7 @@ def fragments(): sign_delegating_key=False, sign_receiving_key=False) - capsule, _ciphertext = encrypt(delegating_pubkey, b'unused') + capsule = MessageKit(delegating_pubkey, b'unused').capsule cfrag = reencrypt(capsule, kfrags[0]) return capsule, cfrag diff --git a/tests/integration/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py index 76ea8597a..66b9ea109 100644 --- a/tests/integration/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -27,7 +27,7 @@ from flask import Response from nucypher.characters.lawful import Ursula from nucypher.crypto.signing import SignatureStamp -from nucypher.crypto.umbral_adapter import SecretKey, Signer, PublicKey, encrypt +from nucypher.crypto.umbral_adapter import SecretKey, Signer, PublicKey from nucypher.datastore.base import RecordField from nucypher.network.nodes import Teacher from tests.markers import skip_on_circleci @@ -107,11 +107,6 @@ def test_alice_verifies_ursula_just_in_time(fleet_of_highperf_mocked_ursulas, highperf_mocked_alice, highperf_mocked_bob): - def mock_encrypt(public_key, plaintext): - if not isinstance(public_key, PublicKey): - public_key = public_key.i_want_to_be_a_real_boy() - return encrypt(public_key, plaintext) - mocks = ( mock_pubkey_from_bytes(), mock_secret_source(), diff --git a/tests/mock/performance_mocks.py b/tests/mock/performance_mocks.py index 23fdd2a5c..14b02d68d 100644 --- a/tests/mock/performance_mocks.py +++ b/tests/mock/performance_mocks.py @@ -173,8 +173,7 @@ class NotARestApp: def actual_rest_app(self): if self._actual_rest_app is None: self._actual_rest_app, self._datastore = make_rest_app(db_filepath=self.db_filepath, - this_node=self.this_node, - domain=None) + this_node=self.this_node) _new_view_functions = self._ViewFunctions(self._actual_rest_app.view_functions) self._actual_rest_app.view_functions = _new_view_functions self._actual_rest_apps.append( diff --git a/tests/unit/characters/control/test_character_fields.py b/tests/unit/characters/control/test_character_fields.py index d22b3a1ec..183e89c49 100644 --- a/tests/unit/characters/control/test_character_fields.py +++ b/tests/unit/characters/control/test_character_fields.py @@ -30,7 +30,6 @@ from nucypher.characters.control.specifications.fields import ( FileField, Key, MessageKit, - UmbralSignature, EncryptedTreasureMap ) from nucypher.characters.lawful import Enrico @@ -135,30 +134,6 @@ def test_message_kit(enacted_federated_policy, federated_alice): field._deserialize(value=b"MessageKit", attr=None, data=None) -def test_umbral_signature(): - umbral_priv_key = SecretKey.random() - signer = Signer(umbral_priv_key) - - message = b'this is a message' - signature = signer.sign(message) - other_signature = signer.sign(b'this is a different message') - - field = UmbralSignature() - serialized = field._serialize(value=signature, attr=None, obj=None) - assert serialized == b64encode(bytes(signature)).decode() - assert serialized != b64encode(bytes(other_signature)).decode() - - deserialized = field._deserialize(value=serialized, attr=None, data=None) - assert deserialized == signature - assert deserialized != other_signature - - field._validate(value=bytes(signature)) - field._validate(value=bytes(other_signature)) - - with pytest.raises(InvalidInputData): - field._validate(value=b"UmbralSignature") - - def test_treasure_map(enacted_federated_policy): treasure_map = enacted_federated_policy.treasure_map diff --git a/tests/unit/test_porter.py b/tests/unit/test_porter.py index fbba4de4b..e8b60cd62 100644 --- a/tests/unit/test_porter.py +++ b/tests/unit/test_porter.py @@ -21,11 +21,11 @@ from eth_utils import to_canonical_address import pytest -from nucypher_core import RetrievalKit as RetrievalKitClass +from nucypher_core import RetrievalKit as RetrievalKitClass, MessageKit from nucypher.control.specifications.exceptions import InvalidInputData from nucypher.control.specifications.fields import StringList -from nucypher.crypto.umbral_adapter import SecretKey, encrypt +from nucypher.crypto.umbral_adapter import SecretKey from nucypher.utilities.porter.control.specifications.fields import UrsulaChecksumAddress from nucypher.utilities.porter.control.specifications.fields.retrieve import RetrievalKit @@ -104,13 +104,13 @@ def test_retrieval_kit_field(get_random_checksum_address): # kit with list of ursulas encrypting_key = SecretKey.random().public_key() - capsule, _ = encrypt(encrypting_key, b'testing retrieval kit with 2 ursulas') + capsule = MessageKit(encrypting_key, b'testing retrieval kit with 2 ursulas').capsule ursulas = [get_random_checksum_address(), get_random_checksum_address()] run_tests_on_kit(kit=RetrievalKitClass(capsule, {to_canonical_address(ursula) for ursula in ursulas})) # kit with no ursulas encrypting_key = SecretKey.random().public_key() - capsule, _ = encrypt(encrypting_key, b'testing retrieval kit with no ursulas') + capsule = MessageKit(encrypting_key, b'testing retrieval kit with no ursulas').capsule run_tests_on_kit(kit=RetrievalKitClass(capsule, set())) with pytest.raises(InvalidInputData): diff --git a/tests/unit/test_versioning.py b/tests/unit/test_versioning.py deleted file mode 100644 index 354ec6092..000000000 --- a/tests/unit/test_versioning.py +++ /dev/null @@ -1,236 +0,0 @@ -""" - This file is part of nucypher. - - nucypher is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - nucypher is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with nucypher. If not, see . -""" - - -import re -from typing import Tuple, Any, Type - -import pytest - -from nucypher.utilities.versioning import Versioned - - -def _check_valid_version_tuple(version: Any, cls: Type): - if not isinstance(version, tuple): - pytest.fail(f"Old version handlers keys for {cls.__name__} must be a tuple") - if not len(version) == Versioned._VERSION_PARTS: - pytest.fail(f"Old version handlers keys for {cls.__name__} must be a {str(Versioned._VERSION_PARTS)}-tuple") - if not all(isinstance(part, int) for part in version): - pytest.fail(f"Old version handlers version parts {cls.__name__} must be integers") - - -class A(Versioned): - - def __init__(self, x: int): - self.x = x - - @classmethod - def _brand(cls): - return b"ABCD" - - @classmethod - def _version(cls) -> Tuple[int, int]: - return 2, 1 - - def _payload(self) -> bytes: - return self.x.to_bytes(1, 'big') - - @classmethod - def _old_version_handlers(cls): - return { - (2, 0): cls._from_bytes_v2_0, - } - - @classmethod - def _from_bytes_v2_0(cls, data): - # v2.0 saved a 4 byte integer in hex format - int_hex, remainder = data[:2], data[2:] - int_bytes = bytes.fromhex(int_hex.decode()) - return cls(int.from_bytes(int_bytes, 'big')), remainder - - @classmethod - def _from_bytes_current(cls, data): - # v2.1 saves a 4 byte integer as 4 bytes - int_bytes, remainder = data[:1], data[1:] - return cls(int.from_bytes(int_bytes, 'big')), remainder - - -def test_unique_branding(): - brands = tuple(v._brand() for v in Versioned.__subclasses__()) - brands_set = set(brands) - if len(brands) != len(brands_set): - duplicate_brands = list(brands) - for brand in brands_set: - duplicate_brands.remove(brand) - pytest.fail(f"Duplicated brand(s) {duplicate_brands}.") - - -def test_valid_branding(): - for cls in Versioned.__subclasses__(): - if len(cls._brand()) != cls._BRAND_SIZE: - pytest.fail(f"Brand must be exactly {str(Versioned._BRAND_SIZE)} bytes.") - if not re.fullmatch(rb'\w+', cls._brand()): - pytest.fail(f"Brand must be alphanumeric; Got {cls._brand()}") - -def test_valid_version_implementation(): - for cls in Versioned.__subclasses__(): - _check_valid_version_tuple(version=cls._version(), cls=cls) - - -def test_valid_old_handlers_index(): - for cls in Versioned.__subclasses__(): - for version in cls._deserializers(): - _check_valid_version_tuple(version=version, cls=cls) - - -def test_version_metadata(): - major, minor = A._version() - assert A.version_string() == f'{major}.{minor}' - - -def test_versioning_header_prepend(): - a = A(1) # stake sauce - assert a.x == 1 - - serialized = bytes(a) - assert len(serialized) > Versioned._HEADER_SIZE - - header = serialized[:Versioned._HEADER_SIZE] - brand = header[:Versioned._BRAND_SIZE] - assert brand == A._brand() - - version = header[Versioned._BRAND_SIZE:] - major, minor = version[:Versioned._VERSION_PART_SIZE], version[Versioned._VERSION_PART_SIZE:] - major_number = int.from_bytes(major, 'big') - minor_number = int.from_bytes(minor, 'big') - assert (major_number, minor_number) == A._version() - - -def test_versioning_input_too_short(): - empty = b'ABCD\x00\x01' - with pytest.raises(ValueError, match='Invalid bytes for A.'): - A.from_bytes(empty) - - -def test_versioning_empty_payload(): - empty = b'ABCD\x00\x02\x00\x01' - with pytest.raises(ValueError, match='No content to deserialize A.'): - A.from_bytes(empty) - - -def test_versioning_invalid_brand(): - invalid = b'\x01\x02\x00\x03\x00\x0112' - with pytest.raises(Versioned.InvalidHeader, match="Incompatible bytes for A."): - A.from_bytes(invalid) - - # A partially invalid brand, to check that the regexp validates - # the whole brand and not just the beginning of it. - invalid = b'ABC \x00\x02\x00\x0112' - with pytest.raises(Versioned.InvalidHeader, match="Incompatible bytes for A."): - A.from_bytes(invalid) - - -def test_versioning_incorrect_brand(): - incorrect = b'ABAB\x00\x0112' - with pytest.raises(Versioned.InvalidHeader, match="Incorrect brand. Expected b'ABCD', Got b'ABAB'."): - A.from_bytes(incorrect) - - -def test_unknown_future_major_version(): - empty = b'ABCD\x00\x03\x00\x0212' - message = 'Incompatible versioned bytes for A. Compatible version is 2.x, Got 3.2.' - with pytest.raises(ValueError, match=message): - A.from_bytes(empty) - - -def test_incompatible_old_major_version(mocker): - current_spy = mocker.spy(A, "_from_bytes_current") - v1_data = b'ABCD\x00\x01\x00\x0012' - message = 'Incompatible versioned bytes for A. Compatible version is 2.x, Got 1.0.' - with pytest.raises(Versioned.IncompatibleVersion, match=message): - A.from_bytes(v1_data) - assert not current_spy.call_count - - -def test_incompatible_future_major_version(mocker): - current_spy = mocker.spy(A, "_from_bytes_current") - v1_data = b'ABCD\x00\x03\x00\x0012' - message = 'Incompatible versioned bytes for A. Compatible version is 2.x, Got 3.0.' - with pytest.raises(Versioned.IncompatibleVersion, match=message): - A.from_bytes(v1_data) - assert not current_spy.call_count - - -def test_resolve_version(): - # past - v2_0 = 2, 0 - resolved_version = A._resolve_version(version=v2_0) - assert resolved_version == v2_0 - - # present - v2_1 = 2, 1 - resolved_version = A._resolve_version(version=v2_1) - assert resolved_version == v2_1 - - # future minor version resolves to the latest minor version. - v2_2 = 2, 2 - resolved_version = A._resolve_version(version=v2_2) - assert resolved_version == v2_1 - - -def test_old_minor_version_handler_routing(mocker): - current_spy = mocker.spy(A, "_from_bytes_current") - v2_0_spy = mocker.spy(A, "_from_bytes_v2_0") - - # Old minor version - v2_0_data = b'ABCD\x00\x02\x00\x0012' - a = A.from_bytes(v2_0_data) - assert a.x == 18 - - # Old minor version was correctly routed to the v2.0 handler. - assert v2_0_spy.call_count == 1 - v2_0_spy.assert_called_with(b'12') - assert not current_spy.call_count - - -def test_current_minor_version_handler_routing(mocker): - current_spy = mocker.spy(A, "_from_bytes_current") - v2_0_spy = mocker.spy(A, "_from_bytes_v2_0") - - v2_1_data = b'ABCD\x00\x02\x00\x01\x12' - a = A.from_bytes(v2_1_data) - assert a.x == 18 - - # Current version was correctly routed to the v2.1 handler. - assert current_spy.call_count == 1 - current_spy.assert_called_with(b'\x12') - assert not v2_0_spy.call_count - - -def test_future_minor_version_handler_routing(mocker): - current_spy = mocker.spy(A, "_from_bytes_current") - v2_0_spy = mocker.spy(A, "_from_bytes_v2_0") - - v2_2_data = b'ABCD\x00\x02\x02\x01\x12' - a = A.from_bytes(v2_2_data) - assert a.x == 18 - - # Future minor version was correctly routed to - # the current minor version handler. - assert current_spy.call_count == 1 - current_spy.assert_called_with(b'\x12') - assert not v2_0_spy.call_count diff --git a/tests/utils/ursula.py b/tests/utils/ursula.py index 07b265df6..7247db3b6 100644 --- a/tests/utils/ursula.py +++ b/tests/utils/ursula.py @@ -26,7 +26,7 @@ from nucypher.blockchain.eth.interfaces import BlockchainInterface from nucypher.characters.lawful import Bob from nucypher.characters.lawful import Ursula from nucypher.config.characters import UrsulaConfiguration -from nucypher.crypto.umbral_adapter import SecretKey, Signer, encrypt, generate_kfrags, reencrypt +from nucypher.crypto.umbral_adapter import SecretKey, Signer, generate_kfrags from tests.constants import NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK from tests.mock.datastore import MOCK_DB