Split treasure maps into decrypted and encrypted

pull/2773/head
Bogdan Opanchuk 2021-08-14 22:00:50 -07:00
parent 5b5cd4bd84
commit 509b8c1bfc
41 changed files with 449 additions and 623 deletions

View File

@ -116,7 +116,7 @@ class AliceInterface(CharacterPublicInterface):
revocation, fail_reason = attempt
if fail_reason == RestMiddleware.NotFound:
del (failed_revocations[node_id])
if len(failed_revocations) <= (policy.n - policy.treasure_map.m + 1):
if len(failed_revocations) <= (policy.n - policy.m + 1):
del (self.implementer.active_policies[policy_id])
response_data = {'failed_revocations': len(failed_revocations)}
@ -192,18 +192,14 @@ class BobInterface(CharacterPublicInterface):
self.implementer.join_policy(label=label, publisher_verifying_key=alice_verifying_key)
if self.implementer.federated_only:
from nucypher.policy.maps import TreasureMap as _MapClass
else:
from nucypher.policy.maps import SignedTreasureMap as _MapClass
from nucypher.policy.maps import EncryptedTreasureMap
# TODO: This LBYL is ugly and fraught with danger. NRN - #2751
if isinstance(treasure_map, bytes):
treasure_map = _MapClass.from_bytes(treasure_map)
treasure_map = EncryptedTreasureMap.from_bytes(treasure_map)
if isinstance(treasure_map, str):
tmap_bytes = treasure_map.encode()
treasure_map = _MapClass.from_bytes(b64decode(tmap_bytes))
treasure_map = EncryptedTreasureMap.from_bytes(b64decode(tmap_bytes))
plaintexts = self.implementer.retrieve(message_kit,
enrico=enrico,

View File

@ -22,30 +22,13 @@ from marshmallow import fields
from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields.base import BaseField
from nucypher.policy.maps import EncryptedTreasureMap as EncryptedTreasureMapClass
class EncryptedTreasureMap(BaseField, fields.Field):
"""
JSON Parameter representation of TreasureMap.
Requires that either federated or non-federated (blockchain) treasure maps are expected to function correcty. This
information is indicated either:
- At creation time of the field via constructor parameter 'federated_only' (takes precedence)
OR
- Via the parent Schema context it is running in. In this case, the parent Schema context dictionary should have a
key-value entry in it with the IS_FEDERATED_CONTEXT_KEY class constant as the key, and a value of True/False.
If neither is provided, the TreasureMap is assumed to be a SignedTreasureMap. The federated/non-federated context
of the TreasureMap only applies to deserialization and validation since the received value is in base64 encoded
bytes.
"""
IS_FEDERATED_CONTEXT_KEY = 'federated'
def __init__(self, federated_only=None, *args, **kwargs):
self.federated_only = federated_only
BaseField.__init__(self, *args, **kwargs)
fields.Field.__init__(self, *args, **kwargs)
def _serialize(self, value, attr, obj, **kwargs):
return b64encode(bytes(value)).decode()
@ -56,23 +39,7 @@ class EncryptedTreasureMap(BaseField, fields.Field):
raise InvalidInputData(f"Could not parse {self.name}: {e}")
def _validate(self, value):
from nucypher.policy.maps import SignedTreasureMap
from nucypher.policy.maps import TreasureMap as UnsignedTreasureMap
# determine context: federated or non-federated defined by field or schema
is_federated_context = False # default to non-federated
if self.federated_only is not None:
# defined by field itself
is_federated_context = self.federated_only
else:
# defined by schema
if self.parent is not None and self.parent.context.get('federated') is not None:
is_federated_context = self.context.get('federated')
try:
splitter = SignedTreasureMap.get_splitter(value) if not is_federated_context else UnsignedTreasureMap.get_splitter(value)
_ = splitter(value)
return True
except InvalidNativeDataTypes as e:
# store exception
raise InvalidInputData(f"Could not parse {self.name} (federated={is_federated_context}): {e}")
EncryptedTreasureMapClass.from_bytes(value)
except Exception as e:
raise InvalidInputData(f"Could not convert input for {self.name} to an EncryptedTreasureMap: {e}") from e

View File

@ -77,7 +77,6 @@ from nucypher.config.constants import END_OF_POLICIES_PROBATIONARY_PERIOD
from nucypher.config.storages import ForgetfulNodeStorage, NodeStorage
from nucypher.control.controllers import WebController
from nucypher.control.emitters import StdoutEmitter
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.keypairs import HostingKeypair
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import (
@ -108,7 +107,7 @@ from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher
from nucypher.network.protocols import InterfaceInfo, parse_node_uri
from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app
from nucypher.network.trackers import AvailabilityTracker
from nucypher.policy.maps import TreasureMap, AuthorizedKeyFrag
from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap, AuthorizedKeyFrag
from nucypher.policy.orders import WorkOrder
from nucypher.policy.policies import Policy
from nucypher.utilities.logging import Logger
@ -404,7 +403,7 @@ class Alice(Character, BlockchainPolicyAuthor):
"""
try:
# Wait for a revocation threshold of nodes to be known ((n - m) + 1)
revocation_threshold = ((policy.n - policy.treasure_map.m) + 1)
revocation_threshold = ((policy.n - policy.m) + 1)
self.block_until_specific_nodes_are_known(
policy.revocation_kit.revokable_addresses,
allow_missing=(policy.n - revocation_threshold))
@ -624,13 +623,13 @@ class Bob(Character):
return unknown_ursulas, known_ursulas, treasure_map.m
def _try_orient(self, treasure_map, publisher_verifying_key):
def _try_orient(self, enc_treasure_map, publisher_verifying_key):
# TODO: should be rather a Publisher character
alice = Alice.from_public_keys(verifying_key=publisher_verifying_key)
compass = self.make_compass_for_alice(alice)
try:
treasure_map.orient(compass)
except treasure_map.InvalidSignature:
return enc_treasure_map.orient(compass)
except EncryptedTreasureMap.InvalidSignature:
raise # TODO: Maybe do something here? NRN
def get_treasure_map(self, publisher_verifying_key: PublicKey, label: bytes):
@ -646,9 +645,9 @@ class Bob(Character):
if not self.known_nodes:
raise self.NotEnoughTeachers("Can't retrieve without knowing about any nodes at all. Pass a teacher or seed node.")
treasure_map = self.get_treasure_map_from_known_ursulas(hrac)
enc_treasure_map = self.get_treasure_map_from_known_ursulas(hrac)
self._try_orient(treasure_map, publisher_verifying_key)
treasure_map = self._try_orient(enc_treasure_map, publisher_verifying_key)
self.treasure_maps[hrac] = treasure_map
return treasure_map
@ -656,7 +655,9 @@ class Bob(Character):
return partial(self.verify_from, alice, decrypt=True)
def construct_policy_hrac(self, publisher_verifying_key: PublicKey, label: bytes) -> bytes:
_hrac = keccak_digest(bytes(publisher_verifying_key) + bytes(self.stamp) + label)[:HRAC_LENGTH]
_hrac = TreasureMap.derive_hrac(publisher_verifying_key=publisher_verifying_key,
bob_verifying_key=self.stamp.as_umbral_pubkey(),
label=label)
return _hrac
def get_treasure_map_from_known_ursulas(self, hrac: bytes, timeout=3):
@ -931,11 +932,11 @@ class Bob(Character):
def _handle_treasure_map(self,
publisher_verifying_key: PublicKey,
label: bytes,
treasure_map: Optional['TreasureMap'] = None,
enc_treasure_map: Optional['EncryptedTreasureMap'] = None,
) -> 'TreasureMap':
if treasure_map is not None:
self._try_orient(treasure_map, publisher_verifying_key)
# self.treasure_maps[treasure_map.hrac] = treasure_map # TODO: Can we?
if enc_treasure_map is not None:
treasure_map = self._try_orient(enc_treasure_map, publisher_verifying_key)
self.treasure_maps[treasure_map.hrac] = treasure_map
else:
hrac = self.construct_policy_hrac(publisher_verifying_key, label)
try:
@ -954,7 +955,7 @@ class Bob(Character):
*message_kits: UmbralMessageKit,
label: bytes,
policy_encrypting_key: Optional[PublicKey] = None,
treasure_map: Optional['TreasureMap'] = None,
treasure_map: Optional['EncryptedTreasureMap'] = None,
# Source Authentication
enrico: Optional["Enrico"] = None,
@ -972,7 +973,7 @@ class Bob(Character):
# If a policy publisher's verifying key is not passed, use Alice's by default.
publisher_verifying_key = alice_verifying_key
treasure_map, m = self._handle_treasure_map(treasure_map=treasure_map,
treasure_map, m = self._handle_treasure_map(enc_treasure_map=treasure_map,
publisher_verifying_key=publisher_verifying_key,
label=label)

View File

@ -28,7 +28,7 @@ from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.utils import encrypt_and_sign
from nucypher.crypto.powers import CryptoPower, SigningPower, DecryptingPower, TransactingPower
from nucypher.exceptions import DevelopmentInstallationRequired
from nucypher.policy.maps import SignedTreasureMap
from nucypher.policy.maps import EncryptedTreasureMap
class Vladimir(Ursula):
@ -113,12 +113,11 @@ class Vladimir(Ursula):
If I see a TreasureMap being published, I can substitute my own payload and hope
that Ursula will store it for me for free.
"""
old_message_kit = legit_treasure_map.message_kit
old_message_kit = legit_treasure_map._encrypted_tmap
new_message_kit, _signature = self.encrypt_for(self, b"I want to store this message for free.")
legit_treasure_map.message_kit = new_message_kit
legit_treasure_map._encrypted_tmap = new_message_kit
# I'll copy Alice's key so that Ursula thinks that the HRAC has been properly signed.
legit_treasure_map.message_kit.sender_verifying_key = old_message_kit.sender_verifying_key
legit_treasure_map._set_payload()
legit_treasure_map._encrypted_tmap.sender_verifying_key = old_message_kit.sender_verifying_key
response = self.network_middleware.put_treasure_map_on_node(node=target_node,
map_payload=bytes(legit_treasure_map))
@ -210,13 +209,13 @@ class Amonia(Alice):
the_map = policy.treasure_map
# I'll make a copy of it to modify for use in this attack.
like_a_map_but_awful = SignedTreasureMap.from_bytes(bytes(the_map))
like_a_map_but_awful = EncryptedTreasureMap.from_bytes(bytes(the_map))
# I'll split the film up into segments, because I know Ursula checks that the file size is under 50k.
for i in range(50):
# I'll include a small portion of this awful film in a new message kit. We don't care about the signature for bob.
not_the_bees = b"Not the bees!" + int(i).to_bytes(length=4, byteorder="big")
like_a_map_but_awful.message_kit, _signature_for_bob_which_is_never_Used = encrypt_and_sign(
like_a_map_but_awful._encrypted_tmap, _signature_for_bob_which_is_never_Used = encrypt_and_sign(
bob.public_keys(DecryptingPower),
plaintext=not_the_bees,
signer=self.stamp,
@ -228,15 +227,15 @@ class Amonia(Alice):
# I know Ursula checks the public signature because she thinks I'm Alice. So I'll sign my bad hrac.
like_a_map_but_awful._public_signature = self.stamp(bytes(self.stamp) + bad_hrac)
like_a_map_but_awful._hrac = bad_hrac
# With the bad hrac and the segment of the film, I'm ready to make a phony payload and map ID.
like_a_map_but_awful._set_payload()
like_a_map_but_awful._set_id()
like_a_map_but_awful.hrac = bad_hrac
# I'll sign it again, so that it appears to match the policy for which I already paid.
transacting_power = self._crypto_power.power_ups(TransactingPower)
like_a_map_but_awful.include_blockchain_signature(blockchain_signer=transacting_power.sign_message)
like_a_map_but_awful._blockchain_signature = EncryptedTreasureMap._make_blockchain_signature(
blockchain_signer=transacting_power.sign_message,
public_signature=like_a_map_but_awful._public_signature,
hrac=like_a_map_but_awful.hrac,
encrypted_tmap=like_a_map_but_awful._encrypted_tmap)
# Sucker.
response = self.network_middleware.put_treasure_map_on_node(sucker_ursula, map_payload=bytes(like_a_map_but_awful))

View File

@ -19,7 +19,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
from cryptography.hazmat.primitives import hashes
# Policy component sizes
HRAC_LENGTH = 16
SIGNATURE_SIZE = 64
EIP712_MESSAGE_SIGNATURE_SIZE = 65

View File

@ -18,7 +18,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
from bytestring_splitter import BytestringSplitter
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.umbral_adapter import CapsuleFrag, PublicKey, Capsule, Signature, KeyFrag
key_splitter = BytestringSplitter((PublicKey, PublicKey.serialized_size()))
@ -26,4 +25,3 @@ 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()))
hrac_splitter = BytestringSplitter((bytes, HRAC_LENGTH))

View File

@ -51,7 +51,7 @@ class Workorder(DatastoreRecord):
class EncryptedTreasureMap(DatastoreRecord):
# Ideally this is a `policy.collections.TreasureMap`, but it causes a huge
# Ideally this is a `policy.maps.EncryptedTreasureMap`, but it causes a huge
# circular import due to `Bob` and `Character` in `policy.collections`.
# TODO #2126
_treasure_map = RecordField(bytes)

View File

@ -308,25 +308,22 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) ->
We set the datastore identifier as the HRAC.
"""
if not this_node.federated_only:
from nucypher.policy.maps import SignedTreasureMap as _MapClass
else:
from nucypher.policy.maps import TreasureMap as _MapClass
from nucypher.policy.maps import EncryptedTreasureMap
# Step 1: First, we verify the signature of the received treasure map.
# This step also deserializes the treasure map if it's signed correctly.
try:
received_treasure_map = _MapClass.from_bytes(bytes_representation=request.data, verify=True)
except _MapClass.InvalidSignature:
log.info(f"Bad TreasureMap HRAC Signature; not storing for HRAC {received_treasure_map._hrac.hex()}")
received_treasure_map = EncryptedTreasureMap.from_bytes(request.data)
except EncryptedTreasureMap.InvalidSignature:
log.info(f"Bad TreasureMap HRAC Signature; not storing for HRAC {received_treasure_map.hrac.hex()}")
return Response("This TreasureMap's HRAC is not properly signed.", status=401)
hrac = received_treasure_map._hrac
hrac = received_treasure_map.hrac
# Step 2: Check if we already have the treasure map.
try:
with datastore.describe(TreasureMapModel, hrac) as stored_treasure_map:
if _MapClass.from_bytes(stored_treasure_map.treasure_map) == received_treasure_map:
if EncryptedTreasureMap.from_bytes(stored_treasure_map.treasure_map) == received_treasure_map:
return Response("Already have this map.", status=303)
except RecordNotFound:
# This appears to be a new treasure map that we don't have!
@ -336,7 +333,7 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) ->
# treasure map is valid pursuant to an active policy.
# We also set the expiration from the data on the blockchain here.
if not this_node.federated_only:
policy = this_node.policy_agent.fetch_policy(policy_id=received_treasure_map._hrac)
policy = this_node.policy_agent.fetch_policy(policy_id=received_treasure_map.hrac)
# If the Policy doesn't exist, the policy_data is all zeros.
if policy.sponsor is NULL_ADDRESS:
log.info(f"TreasureMap is for non-existent Policy; not storing {hrac.hex()}")

View File

@ -23,6 +23,7 @@ from nucypher.acumen.perception import FleetSensor
from nucypher.crypto.signing import InvalidSignature
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.network.nodes import Learner
from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap
def get_treasure_map_from_known_ursulas(learner: Learner,
@ -33,11 +34,6 @@ def get_treasure_map_from_known_ursulas(learner: Learner,
Iterate through the nodes we know, asking for the TreasureMap.
Return the first one who has it.
"""
if learner.federated_only:
from nucypher.policy.maps import TreasureMap as _MapClass
else:
from nucypher.policy.maps import SignedTreasureMap as _MapClass
start = maya.now()
# Spend no more than half the timeout finding the nodes. 8 nodes is arbitrary. Come at me.
@ -63,7 +59,7 @@ def get_treasure_map_from_known_ursulas(learner: Learner,
if response.status_code == 200 and response.content:
try:
treasure_map = _MapClass.from_bytes(response.content)
treasure_map = EncryptedTreasureMap.from_bytes(response.content)
return treasure_map
except InvalidSignature:
# TODO: What if a node gives a bunk TreasureMap? NRN
@ -74,8 +70,8 @@ def get_treasure_map_from_known_ursulas(learner: Learner,
learner.learn_from_teacher_node()
if (start - maya.now()).seconds > timeout:
raise _MapClass.NowhereToBeFound(f"Asked {len(learner.known_nodes)} nodes, "
f"but none had map {hrac.hex()}")
raise TreasureMap.NowhereToBeFound(f"Asked {len(learner.known_nodes)} nodes, "
f"but none had map {hrac.hex()}")
def find_matching_nodes(known_nodes: FleetSensor,

View File

@ -16,31 +16,150 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import Optional, Callable, Union, Sequence
from typing import Optional, Callable, Union, Sequence, Dict
from bytestring_splitter import (
BytestringSplitter,
VariableLengthBytestring,
BytestringKwargifier,
BytestringSplittingError,
VersioningMixin,
BrandingMixin
)
from constant_sorrow.constants import NO_DECRYPTION_PERFORMED, NOT_SIGNED
from eth_utils.address import to_checksum_address, to_canonical_address
from eth_typing.evm import ChecksumAddress
from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH
from nucypher.characters.base import Character
from nucypher.crypto.constants import HRAC_LENGTH, EIP712_MESSAGE_SIGNATURE_SIZE
from nucypher.crypto.constants import EIP712_MESSAGE_SIGNATURE_SIZE
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.crypto.signing import SignatureStamp
from nucypher.crypto.splitters import signature_splitter, hrac_splitter, kfrag_splitter
from nucypher.crypto.splitters import signature_splitter, kfrag_splitter
from nucypher.crypto.umbral_adapter import KeyFrag, VerifiedKeyFrag, PublicKey, Signature
from nucypher.crypto.utils import keccak_digest, encrypt_and_sign, verify_eip_191
from nucypher.network.middleware import RestMiddleware
class TreasureMap:
HRAC_LENGTH = 16
class NowhereToBeFound(RestMiddleware.NotFound):
"""
Called when no known nodes have it.
"""
class IsDisorienting(Exception):
"""
Called when an oriented TreasureMap lists fewer than m destinations, which
leaves Bob disoriented.
"""
main_splitter = BytestringSplitter(
(int, 1, {'byteorder': 'big'}),
(bytes, HRAC_LENGTH),
)
ursula_and_kfrag_payload_splitter = BytestringSplitter(
(to_checksum_address, ETH_ADDRESS_BYTE_LENGTH),
(UmbralMessageKit, VariableLengthBytestring),
)
@staticmethod
def derive_hrac(publisher_verifying_key: PublicKey, bob_verifying_key: PublicKey, label: bytes) -> bytes:
"""
Here's our "hashed resource access code".
A hash of:
* Alice's public key
* Bob's public key
* the label
Alice 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.
"""
return keccak_digest(bytes(publisher_verifying_key) + bytes(bob_verifying_key) + label)[:TreasureMap.HRAC_LENGTH]
@classmethod
def construct_by_publisher(cls,
publisher: 'Alice',
bob: 'Bob',
label: bytes,
ursulas: Sequence['Ursula'],
verified_kfrags: Sequence[VerifiedKeyFrag],
m: int,
) -> 'TreasureMap':
"""Create a new treasure map for a collection of ursulas and kfrags."""
if m < 1 or m > 255:
raise ValueError("The threshold must be between 1 and 255.")
if len(ursulas) < m:
raise ValueError(
f"The number of destinations ({len(ursulas)}) "
f"must be equal or greater than the threshold ({m})")
# The HRAC is needed to produce kfrag writs.
hrac = cls.derive_hrac(publisher_verifying_key=publisher.stamp.as_umbral_pubkey(),
bob_verifying_key=bob.public_keys(SigningPower),
label=label)
# Encrypt each kfrag for an Ursula.
destinations = {}
for ursula, verified_kfrag in zip(ursulas, verified_kfrags):
kfrag_payload = bytes(AuthorizedKeyFrag.construct_by_publisher(hrac=hrac,
verified_kfrag=verified_kfrag,
publisher_stamp=publisher.stamp))
encrypted_kfrag, _signature = encrypt_and_sign(recipient_pubkey_enc=ursula.public_keys(DecryptingPower),
plaintext=kfrag_payload,
signer=publisher.stamp)
destinations[ursula.checksum_address] = encrypted_kfrag
return cls(m=m, hrac=hrac, destinations=destinations)
def __init__(self,
m: int,
hrac: bytes,
destinations: Dict[ChecksumAddress, bytes],
):
self.m = m
self.destinations = destinations
self.hrac = hrac
def prepare_for_publication(self, publisher, bob, blockchain_signer=None):
return EncryptedTreasureMap.construct_by_publisher(
self, publisher, bob, blockchain_signer=blockchain_signer)
def _nodes_as_bytes(self):
nodes_as_bytes = b""
for ursula_address, encrypted_kfrag in self.destinations.items():
node_id = to_canonical_address(ursula_address)
kfrag = bytes(VariableLengthBytestring(encrypted_kfrag.to_bytes()))
nodes_as_bytes += (node_id + kfrag)
return nodes_as_bytes
def __bytes__(self):
return self.m.to_bytes(1, "big") + self.hrac + self._nodes_as_bytes()
@classmethod
def from_bytes(cls, data):
try:
m, hrac, remainder = cls.main_splitter(data, return_remainder=True)
ursula_and_kfrags = cls.ursula_and_kfrag_payload_splitter.repeat(remainder)
except BytestringSplittingError as e:
raise cls.IsDisorienting('Invalid treasure map contents.') from e
destinations = {u: k for u, k in ursula_and_kfrags}
return cls(m, hrac, destinations)
def __iter__(self):
return iter(self.destinations.items())
def __len__(self):
return len(self.destinations)
class AuthorizedKeyFrag:
_WRIT_CHECKSUM_SIZE = 32
@ -50,8 +169,8 @@ class AuthorizedKeyFrag:
ENCRYPTED_SIZE = 619
_splitter = BytestringSplitter(
hrac_splitter, # HRAC
BytestringSplitter((bytes, _WRIT_CHECKSUM_SIZE)), # kfrag checksum
(bytes, TreasureMap.HRAC_LENGTH), # HRAC
(bytes, _WRIT_CHECKSUM_SIZE), # kfrag checksum
signature_splitter, # Publisher's signature
kfrag_splitter,
)
@ -103,215 +222,49 @@ class AuthorizedKeyFrag:
return cls(hrac, kfrag_checksum, writ_signature, kfrag)
class TreasureMapSplitter(BrandingMixin, VersioningMixin, BytestringKwargifier):
pass
class EncryptedTreasureMap:
class TreasureMap:
VERSION_NUMBER = 1 # Increment when serialization format changes.
_BRAND = b'TM'
_VERSION = int(VERSION_NUMBER).to_bytes(VersioningMixin.HEADER_LENGTH, 'big')
class NowhereToBeFound(RestMiddleware.NotFound):
"""
Called when no known nodes have it.
"""
class IsDisorienting(Exception):
"""
Called when an oriented TreasureMap lists fewer than m destinations, which
leaves Bob disoriented.
"""
class OldVersion(Exception):
"""Raised when a treasure map's version is too old or contents are incompatible."""
ursula_and_kfrag_payload_splitter = BytestringSplitter(
(to_checksum_address, ETH_ADDRESS_BYTE_LENGTH),
_splitter = BytestringSplitter(
signature_splitter,
(bytes, TreasureMap.HRAC_LENGTH),
(UmbralMessageKit, VariableLengthBytestring),
)
(bytes, 1))
from nucypher.crypto.signing import \
InvalidSignature # Raised when the public signature (typically intended for Ursula) is not valid.
def __init__(self,
m: int = None,
destinations=None,
message_kit: UmbralMessageKit = None,
public_signature: Signature = None,
hrac: Optional[bytes] = None,
version: bytes = None
) -> None:
@staticmethod
def _make_blockchain_signature(blockchain_signer, public_signature, hrac, encrypted_tmap):
# This method exists mainly to link this scheme to the corresponding test
payload = bytes(public_signature) + hrac + encrypted_tmap.to_bytes()
return blockchain_signer(payload)
if version is not None:
self.version = version
@classmethod
def construct_by_publisher(cls, treasure_map, publisher, bob, blockchain_signer=None):
# TODO: `publisher` 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?
if m is not None:
if m > 255:
raise ValueError("Largest allowed value for m is 255.")
self._m = m
bob_encrypting_key = bob.public_keys(DecryptingPower)
self._destinations = destinations or {}
encrypted_tmap, _signature_for_bob = encrypt_and_sign(bob_encrypting_key,
plaintext=bytes(treasure_map),
signer=publisher.stamp)
public_signature = publisher.stamp(bytes(publisher.stamp) + treasure_map.hrac)
if blockchain_signer is not None:
blockchain_signature = EncryptedTreasureMap._make_blockchain_signature(
blockchain_signer, public_signature, treasure_map.hrac, encrypted_tmap)
else:
self._m = NO_DECRYPTION_PERFORMED
self._destinations = NO_DECRYPTION_PERFORMED
blockchain_signature = None
self._id = None
return cls(treasure_map.hrac, public_signature, encrypted_tmap, blockchain_signature=blockchain_signature)
self.message_kit = message_kit
def __init__(self, hrac, public_signature, encrypted_tmap, blockchain_signature=None):
self.hrac = hrac
self._public_signature = public_signature
self._hrac = hrac
self._payload = None
if message_kit is not None:
self.message_kit = message_kit
self._set_id()
else:
self.message_kit = None
def __eq__(self, other):
try:
return self.public_id() == other.public_id()
except AttributeError:
raise TypeError(
f"Can't compare {type(other).__name__} to a TreasureMap (it needs to implement public_id() )")
def __iter__(self):
return iter(self.destinations.items())
def __len__(self):
return len(self.destinations)
def __bytes__(self):
if self._payload is None:
self._set_payload()
return self._BRAND + self._VERSION + self._payload
@classmethod
def get_splitter(cls, bytes_representation: bytes) -> BytestringKwargifier:
"""
Takes a bytes representation of a treasure map and returns a splitter matching the apparent format.
In the event of a missing or malformed header, returns the splitter designed for unversioned maps.
"""
representation_metadata = TreasureMapSplitter.get_metadata(bytes_representation)
# header = bytes_representation[:cls._HEADER_SIZE]
brand_matches = representation_metadata['brand'] == cls._BRAND
version = representation_metadata['version']
if version in cls._splitters and brand_matches:
# This representation is compatible with a known stencil.
splitter = cls._splitters[version]
else:
# It's possible that this is a preversioned representation.
splitter = cls._splitters['unversioned'] # TODO: In this case, it's still a map from a previous version - how will we handle sin KFrags?
return splitter
@classmethod
def from_bytes(cls, bytes_representation: bytes, verify: bool = True) -> Union['TreasureMap', 'SignedTreasureMap']:
splitter = cls.get_splitter(bytes_representation)
treasure_map = splitter(bytes_representation)
if verify:
treasure_map.public_verify()
return treasure_map
@property
def _verifying_key(self):
return self.message_kit.sender_verifying_key
@property
def m(self) -> int:
if self._m == NO_DECRYPTION_PERFORMED:
raise TypeError("The TreasureMap is probably encrypted. You must decrypt it first.")
return self._m
@property
def destinations(self):
if self._destinations == NO_DECRYPTION_PERFORMED:
raise TypeError("The TreasureMap is probably encrypted. You must decrypt it first.")
return self._destinations
def _set_id(self) -> None:
self._id = keccak_digest(bytes(self._verifying_key) + bytes(self._hrac)).hex()
def _set_payload(self) -> None:
self._payload = bytes(self._public_signature) \
+ self._hrac \
+ bytes(VariableLengthBytestring(self.message_kit.to_bytes()))
def derive_hrac(self, publisher_stamp: SignatureStamp, bob_verifying_key: PublicKey, label: bytes) -> None:
"""
Here's our "hashed resource access code".
A hash of:
* Alice's public key
* Bob's public key
* the label
Alice 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.
"""
self._hrac = keccak_digest(bytes(publisher_stamp) + bytes(bob_verifying_key) + label)[:HRAC_LENGTH]
def prepare_for_publication(self, bob_encrypting_key, publisher_stamp):
plaintext = self._m.to_bytes(1, "big") + self.nodes_as_bytes()
self.message_kit, _signature_for_bob = encrypt_and_sign(bob_encrypting_key,
plaintext=plaintext,
signer=publisher_stamp)
self._public_signature = publisher_stamp(bytes(publisher_stamp) + self._hrac)
self._set_payload()
self._set_id()
def nodes_as_bytes(self):
if self.destinations == NO_DECRYPTION_PERFORMED:
return NO_DECRYPTION_PERFORMED
else:
nodes_as_bytes = b""
for ursula_address, encrypted_kfrag in self.destinations.items():
node_id = to_canonical_address(ursula_address)
kfrag = bytes(VariableLengthBytestring(encrypted_kfrag.to_bytes()))
nodes_as_bytes += (node_id + kfrag)
return nodes_as_bytes
def add_kfrag(self, ursula, verified_kfrag: VerifiedKeyFrag, publisher_stamp: SignatureStamp) -> None:
if self.destinations == NO_DECRYPTION_PERFORMED:
# Unsure how this situation can arise, but let's raise an error just in case.
raise TypeError("This TreasureMap is encrypted. You can't add another node without decrypting it.")
if not self._hrac:
# TODO: Use a better / different exception or encapsulate HRAC derivation with KFrag population.
raise RuntimeError(
'Cannot add KFrag to TreasureMap without an HRAC set. Call "derive_hrac" and try again.')
# Encrypt this kfrag payload for Ursula.
kfrag_payload = bytes(AuthorizedKeyFrag.construct_by_publisher(hrac=self._hrac,
verified_kfrag=verified_kfrag,
publisher_stamp=publisher_stamp))
encrypted_kfrag, _signature = encrypt_and_sign(recipient_pubkey_enc=ursula.public_keys(DecryptingPower),
plaintext=kfrag_payload,
signer=publisher_stamp)
# Set the encrypted kfrag payload into the map.
self.destinations[ursula.checksum_address] = encrypted_kfrag
def public_id(self) -> str:
"""
We need an ID that Bob can glean from knowledge he already has *and* which Ursula can verify came from Alice.
Ursula will refuse to propagate this if it she can't prove the payload is signed by Alice's public key,
which is included in it,
"""
return self._id
def public_verify(self) -> bool:
message = bytes(self._verifying_key) + self._hrac
verified = self._public_signature.verify(self._verifying_key, message=message)
if verified:
return True
else:
raise self.InvalidSignature("This TreasureMap is not properly publicly signed by Alice.")
self._verifying_key = encrypted_tmap.sender_verifying_key
self._encrypted_tmap = encrypted_tmap
self._blockchain_signature = blockchain_signature
def orient(self, compass: Callable):
"""
@ -319,113 +272,47 @@ class TreasureMap:
payload message kit).
"""
try:
map_in_the_clear = compass(message_kit=self.message_kit)
map_in_the_clear = compass(self._encrypted_tmap)
except Character.InvalidSignature:
raise self.InvalidSignature("This TreasureMap does not contain the correct signature from Alice to Bob.")
self._m = map_in_the_clear[0]
try:
ursula_and_kfrags = self.ursula_and_kfrag_payload_splitter.repeat(map_in_the_clear[1:])
except BytestringSplittingError:
raise self.IsDisorienting('Invalid treasure map contents.')
self._destinations = {u: k for u, k in ursula_and_kfrags}
self.check_for_sufficient_destinations() # TODO: Remove this check, how is this even possible?
def check_for_sufficient_destinations(self):
if len(self._destinations) < self._m or self._m == 0:
raise self.IsDisorienting(
f"TreasureMap lists only {len(self._destinations)} destination, "
f"but requires interaction with {self._m} nodes.")
return TreasureMap.from_bytes(map_in_the_clear)
@classmethod
def construct_by_publisher(cls,
publisher: 'Alice',
bob: 'Bob',
label: bytes,
ursulas: Sequence['Ursula'],
verified_kfrags: Sequence[VerifiedKeyFrag],
m: int
) -> 'TreasureMap':
"""Create a new treasure map for a collection of ursulas and kfrags."""
# The HRAC is needed to produce kfrag writs.
treasure_map = cls(m=m)
treasure_map.derive_hrac(publisher_stamp=publisher.stamp,
bob_verifying_key=bob.public_keys(SigningPower),
label=label)
# Encrypt each kfrag for an Ursula.
for ursula, verified_kfrag in zip(ursulas, verified_kfrags):
treasure_map.add_kfrag(ursula=ursula,
verified_kfrag=verified_kfrag,
publisher_stamp=publisher.stamp)
# Sign the map if needed before sending it out into the world.
treasure_map.prepare_for_publication(bob_encrypting_key=bob.public_keys(DecryptingPower),
publisher_stamp=publisher.stamp)
return treasure_map
# FIXME: a dirty hack to make the tests pass. Fix it ASAP.
# The problem with __new__ is that it does not get called before the first object of the class
# is instantiated, so when we call `from_bytes()` for the first time, the `_splitters` dict
# needs to already be populated.
TreasureMap._splitters = {
'unversioned': BytestringKwargifier(TreasureMap,
public_signature=signature_splitter,
hrac=(bytes, HRAC_LENGTH),
message_kit=(UmbralMessageKit, VariableLengthBytestring),
),
1: TreasureMapSplitter(TreasureMap,
public_signature=signature_splitter,
hrac=(bytes, HRAC_LENGTH),
message_kit=(UmbralMessageKit, VariableLengthBytestring),
)
}
class SignedTreasureMap(TreasureMap):
_BRAND = b'SM'
def __init__(self, blockchain_signature=NOT_SIGNED, *args, **kwargs):
self._blockchain_signature = blockchain_signature
super().__init__(*args, **kwargs)
def include_blockchain_signature(self, blockchain_signer):
if self._payload is None:
self._set_payload()
self._blockchain_signature = blockchain_signer(self._payload)
def __bytes__(self):
return (bytes(self._public_signature) +
self.hrac +
bytes(VariableLengthBytestring(self._encrypted_tmap.to_bytes())) +
# Other variants:
# - store all zeros if no blockchain signature
# - store the flag too in addition to all zeros, for added sanity check
# - use a faux blockchain signer in federated case
(b'\x00' if self._blockchain_signature is None else (b'\x01' + bytes(self._blockchain_signature)))
)
def verify_blockchain_signature(self, checksum_address):
self._set_payload()
return verify_eip_191(message=self._payload,
if self._blockchain_signature is None:
raise ValueError("This EncryptedTreasureMap is not blockchain-signed")
payload = bytes(self._public_signature) + self.hrac + self._encrypted_tmap.to_bytes()
return verify_eip_191(message=payload,
signature=self._blockchain_signature,
address=checksum_address)
def __bytes__(self):
if self._blockchain_signature is NOT_SIGNED:
raise self.InvalidSignature(
"Can't cast a SignedTreasureMap to bytes until it has a blockchain signature "
"(otherwise, is it really a 'SignedTreasureMap'?")
if self._payload is None:
self._set_payload()
return self._BRAND + self._VERSION + self._blockchain_signature + self._payload
def _public_verify(self) -> bool:
message = bytes(self._verifying_key) + self.hrac
if not self._public_signature.verify(self._verifying_key, message=message):
raise self.InvalidSignature("This TreasureMap is not properly publicly signed by Alice.")
@classmethod
def from_bytes(cls, data):
try:
public_signature, hrac, message_kit, bc_sig, remainder = cls._splitter(data, return_remainder=True)
if bc_sig == b'\x01':
blockchain_signature, = BytestringSplitter((bytes, EIP712_MESSAGE_SIGNATURE_SIZE))(remainder)
else:
blockchain_signature = None
except BytestringSplittingError as e:
raise TreasureMap.IsDisorienting('Invalid treasure map contents.') from e
# FIXME: a dirty hack to make the tests pass. Fix it ASAP.
# See the comment at `TreasureMap._splitters` above.
SignedTreasureMap._splitters = {
'unversioned': BytestringKwargifier(SignedTreasureMap,
blockchain_signature=EIP712_MESSAGE_SIGNATURE_SIZE,
public_signature=signature_splitter,
hrac=(bytes, HRAC_LENGTH),
message_kit=(UmbralMessageKit, VariableLengthBytestring),
),
1: TreasureMapSplitter(SignedTreasureMap,
blockchain_signature=EIP712_MESSAGE_SIGNATURE_SIZE,
public_signature=signature_splitter,
hrac=(bytes, HRAC_LENGTH),
message_kit=(UmbralMessageKit, VariableLengthBytestring),
)
}
result = cls(hrac, public_signature, message_kit, blockchain_signature=blockchain_signature)
result._public_verify()
return result

View File

@ -32,7 +32,6 @@ from nucypher.crypto.splitters import (
capsule_splitter,
cfrag_splitter,
signature_splitter,
hrac_splitter,
kfrag_splitter
)
from nucypher.crypto.umbral_adapter import (
@ -41,7 +40,7 @@ from nucypher.crypto.umbral_adapter import (
PublicKey,
Signature
)
from nucypher.policy.maps import AuthorizedKeyFrag
from nucypher.policy.maps import AuthorizedKeyFrag, TreasureMap
class WorkOrder:
@ -59,7 +58,7 @@ class WorkOrder:
+ key_splitter \
+ key_splitter \
+ key_splitter \
+ hrac_splitter \
+ BytestringSplitter((bytes, TreasureMap.HRAC_LENGTH)) \
+ BytestringSplitter((bytes, VariableLengthBytestring))
class PRETask:

View File

@ -26,14 +26,14 @@ from eth_typing.evm import ChecksumAddress
from twisted.internet import reactor
from nucypher.blockchain.eth.constants import POLICY_ID_LENGTH
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.kits import RevocationKit
from nucypher.crypto.powers import TransactingPower
from nucypher.crypto.powers import TransactingPower, DecryptingPower
from nucypher.crypto.splitters import key_splitter
from nucypher.crypto.utils import keccak_digest
from nucypher.crypto.umbral_adapter import PublicKey, VerifiedKeyFrag, Signature
from nucypher.crypto.utils import construct_policy_id
from nucypher.network.middleware import RestMiddleware
from nucypher.policy.maps import TreasureMap
from nucypher.policy.reservoir import (
make_federated_staker_reservoir,
MergedReservoir,
@ -231,7 +231,9 @@ class Policy(ABC):
Alice and Bob have all the information they need to construct this.
'Ursula' does not, so we share it with her.
"""
self.hrac = keccak_digest(bytes(self.publisher.stamp) + bytes(self.bob.stamp) + self.label)[:HRAC_LENGTH]
self.hrac = TreasureMap.derive_hrac(publisher_verifying_key=self.publisher.stamp.as_umbral_pubkey(),
bob_verifying_key=self.bob.stamp.as_umbral_pubkey(),
label=self.label)
def __repr__(self):
return f"{self.__class__.__name__}:{self._id.hex()[:6]}"
@ -342,21 +344,8 @@ class Policy(ABC):
return accepted_arrangements
def _make_treasure_map(self,
network_middleware: RestMiddleware,
arrangements: Dict['Ursula', Arrangement],
) -> 'TreasureMap':
"""Author a new treasure map for this policy as Alice.."""
treasure_map = self._treasure_map_class.construct_by_publisher(publisher=self.publisher,
bob=self.bob,
label=self.label,
ursulas=list(arrangements),
verified_kfrags=self.kfrags,
m=self.m)
return treasure_map
def _make_publisher(self,
treasure_map: 'TreasureMap',
treasure_map: 'EncryptedTreasureMap',
network_middleware: RestMiddleware,
) -> TreasureMapPublisher:
@ -369,6 +358,9 @@ class Policy(ABC):
nodes=target_nodes,
network_middleware=network_middleware)
def _encrypt_treasure_map(self, treasure_map):
return treasure_map.prepare_for_publication(self.publisher, self.bob)
def enact(self,
network_middleware: RestMiddleware,
handpicked_ursulas: Optional[Iterable['Ursula']] = None,
@ -388,18 +380,27 @@ class Policy(ABC):
self._enact_arrangements(arrangements)
treasure_map = self._make_treasure_map(network_middleware=network_middleware,
arrangements=arrangements)
treasure_map_publisher = self._make_publisher(treasure_map=treasure_map,
treasure_map = TreasureMap.construct_by_publisher(publisher=self.publisher,
bob=self.bob,
label=self.label,
ursulas=list(arrangements),
verified_kfrags=self.kfrags,
m=self.m)
enc_treasure_map = self._encrypt_treasure_map(treasure_map)
treasure_map_publisher = self._make_publisher(treasure_map=enc_treasure_map,
network_middleware=network_middleware)
revocation_kit = RevocationKit(treasure_map=treasure_map, signer=self.publisher.stamp) # TODO: Signal revocation without using encrypted kfrag
# TODO: Signal revocation without using encrypted kfrag
revocation_kit = RevocationKit(treasure_map=treasure_map, signer=self.publisher.stamp)
enacted_policy = EnactedPolicy(self._id,
self.hrac,
self.label,
self.public_key,
treasure_map,
treasure_map.m,
enc_treasure_map,
treasure_map_publisher,
revocation_kit,
self.publisher.stamp.as_umbral_pubkey())
@ -426,8 +427,6 @@ class Policy(ABC):
class FederatedPolicy(Policy):
from nucypher.policy.maps import TreasureMap as __map_class
_treasure_map_class = __map_class
def _not_enough_ursulas_exception(self):
return Policy.NotEnoughUrsulas
@ -445,9 +444,6 @@ class BlockchainPolicy(Policy):
A collection of n Arrangements representing a single Policy
"""
from nucypher.policy.maps import SignedTreasureMap as __map_class
_treasure_map_class = __map_class
class InvalidPolicyValue(ValueError):
pass
@ -542,15 +538,12 @@ class BlockchainPolicy(Policy):
def _enact_arrangements(self, arrangements: Dict['Ursula', Arrangement]) -> None:
self._publish_to_blockchain(ursulas=list(arrangements))
def _make_treasure_map(self,
network_middleware: RestMiddleware,
arrangements: Dict['Ursula', Arrangement],
) -> 'TreasureMap':
treasure_map = super()._make_treasure_map(network_middleware, arrangements)
def _encrypt_treasure_map(self, treasure_map):
transacting_power = self.publisher._crypto_power.power_ups(TransactingPower)
treasure_map.include_blockchain_signature(transacting_power.sign_message)
return treasure_map
return treasure_map.prepare_for_publication(
self.publisher,
self.bob,
blockchain_signer=transacting_power.sign_message)
class EnactedPolicy:
@ -560,7 +553,8 @@ class EnactedPolicy:
hrac: bytes,
label: bytes,
public_key: PublicKey,
treasure_map: 'TreasureMap',
m: int,
treasure_map: 'EncryptedTreasureMap',
treasure_map_publisher: TreasureMapPublisher,
revocation_kit: RevocationKit,
publisher_verifying_key: PublicKey,
@ -573,7 +567,8 @@ class EnactedPolicy:
self.treasure_map = treasure_map
self.treasure_map_publisher = treasure_map_publisher
self.revocation_kit = revocation_kit
self.n = len(self.treasure_map.destinations)
self.m = m
self.n = len(self.revocation_kit)
self.publisher_verifying_key = publisher_verifying_key
def publish_treasure_map(self):

View File

@ -19,7 +19,6 @@ from typing import List, Optional
from eth_typing import ChecksumAddress
from nucypher.crypto.umbral_adapter import PublicKey
from nucypher.characters.control.specifications.fields import EncryptedTreasureMap
from nucypher.control.interfaces import ControlInterface, attach_schema
from nucypher.utilities.porter.control.specifications import porter_schema
@ -27,8 +26,6 @@ from nucypher.utilities.porter.control.specifications import porter_schema
class PorterInterface(ControlInterface):
def __init__(self, porter: 'Porter' = None, *args, **kwargs):
super().__init__(implementer=porter, *args, **kwargs)
# set federated/non-federated context for publish treasure map schema
PorterInterface.publish_treasure_map._schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = porter.federated_only
#
# Alice Endpoints

View File

@ -20,7 +20,7 @@ from marshmallow import fields
from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields.base import BaseField
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.policy.maps import TreasureMap
class HRAC(BaseField, fields.String):
@ -40,5 +40,5 @@ class HRAC(BaseField, fields.String):
if not isinstance(value, bytes):
raise InvalidInputData(f"Could not convert input for {self.name} to a valid TreasureMap HRAC: must be a bytestring")
if len(value) != HRAC_LENGTH:
if len(value) != TreasureMap.HRAC_LENGTH:
raise InvalidInputData(f"Could not convert input for {self.name} to a valid TreasureMap HRAC: invalid length")

View File

@ -22,13 +22,18 @@ from nucypher.characters.control.interfaces import AliceInterface
from nucypher.characters.control.interfaces import BobInterface, EnricoInterface
from nucypher.crypto.constants import EIP712_MESSAGE_SIGNATURE_SIZE
from nucypher.crypto.powers import DecryptingPower
from nucypher.policy.maps import SignedTreasureMap
from nucypher.policy.maps import TreasureMap
from tests.utils.controllers import get_fields, validate_json_rpc_response_data
def test_bob_rpc_character_control_join_policy(bob_rpc_controller, join_control_request, enacted_blockchain_policy, blockchain_bob, blockchain_ursulas):
def test_bob_rpc_character_control_join_policy(bob_rpc_controller,
join_control_request,
blockchain_treasure_map,
enacted_blockchain_policy,
blockchain_bob,
blockchain_ursulas):
for ursula in blockchain_ursulas:
if ursula.checksum_address in enacted_blockchain_policy.treasure_map.destinations:
if ursula.checksum_address in blockchain_treasure_map.destinations:
# Simulate passing in a teacher-uri
blockchain_bob.remember_node(ursula)
break
@ -68,21 +73,13 @@ def test_bob_rpc_character_control_retrieve_with_tmap(
response = bob_rpc_controller.send(request_data)
assert response.data['result']['cleartexts'][0] == 'Welcome to flippering number 1.'
# Make a wrong (empty) treasure map
# Make a wrong treasure map
enc_wrong_tmap = bytes(enacted_blockchain_policy.treasure_map)[:-1]
wrong_tmap = SignedTreasureMap(m=0)
wrong_tmap.derive_hrac(publisher_stamp=blockchain_alice.stamp, bob_verifying_key=blockchain_bob.stamp, label=b"Wrong")
wrong_tmap.prepare_for_publication(
bob_encrypting_key=blockchain_bob.public_keys(DecryptingPower),
publisher_stamp=blockchain_alice.stamp)
wrong_tmap._blockchain_signature = b"this is not a signature, but we don't need one for this test....." # ...because it only matters when Ursula looks at it. (65 bytes)
assert len(wrong_tmap._blockchain_signature) == EIP712_MESSAGE_SIGNATURE_SIZE
tmap_bytes = bytes(wrong_tmap)
tmap_bytes = bytes(enc_wrong_tmap)
tmap_64 = b64encode(tmap_bytes).decode()
request_data['params']['treasure_map'] = tmap_64
with pytest.raises(SignedTreasureMap.IsDisorienting):
with pytest.raises(TreasureMap.IsDisorienting):
bob_rpc_controller.send(request_data)

View File

@ -25,7 +25,7 @@ from nucypher.control.specifications.exceptions import InvalidInputData
def test_treasure_map(enacted_blockchain_policy):
treasure_map = enacted_blockchain_policy.treasure_map
field = EncryptedTreasureMap(federated_only=False)
field = EncryptedTreasureMap()
serialized = field._serialize(value=treasure_map, attr=None, obj=None)
assert serialized == b64encode(bytes(treasure_map)).decode()

View File

@ -26,7 +26,7 @@ from click.testing import CliRunner
import nucypher
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import DecryptingPower
from nucypher.policy.maps import TreasureMap, SignedTreasureMap
from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap
click_runner = CliRunner()
@ -89,8 +89,8 @@ def test_alice_web_character_control_grant(alice_web_controller_test_client, gra
assert 'alice_verifying_key' in response_data['result']
map_bytes = b64decode(response_data['result']['treasure_map'])
encrypted_map = SignedTreasureMap.from_bytes(map_bytes)
assert encrypted_map._hrac is not None
encrypted_map = EncryptedTreasureMap.from_bytes(map_bytes)
assert encrypted_map.hrac is not None
# Send bad data to assert error returns
response = alice_web_controller_test_client.put(endpoint, data=json.dumps({'bad': 'input'}))
@ -175,14 +175,19 @@ def test_alice_character_control_decrypt(alice_web_controller_test_client,
assert response.status_code == 405
def test_bob_character_control_join_policy(bob_web_controller_test_client, enacted_blockchain_policy, blockchain_alice, blockchain_bob, blockchain_ursulas):
def test_bob_character_control_join_policy(bob_web_controller_test_client,
blockchain_treasure_map,
enacted_blockchain_policy,
blockchain_alice,
blockchain_bob,
blockchain_ursulas):
request_data = {
'label': enacted_blockchain_policy.label.decode(),
'publisher_verifying_key': bytes(enacted_blockchain_policy.publisher_verifying_key).hex(),
}
for ursula in blockchain_ursulas:
if ursula.checksum_address in enacted_blockchain_policy.treasure_map.destinations:
if ursula.checksum_address in blockchain_treasure_map.destinations:
# Simulate passing in a teacher-uri
blockchain_bob.remember_node(ursula)
break

View File

@ -24,7 +24,7 @@ import pytest
from nucypher.crypto.kits import PolicyMessageKit
from nucypher.crypto.utils import keccak_digest
from nucypher.datastore.models import EncryptedTreasureMap as DatastoreTreasureMap
from nucypher.policy.maps import SignedTreasureMap
from nucypher.policy.maps import EncryptedTreasureMap
def test_decentralized_grant(blockchain_alice, blockchain_bob, blockchain_ursulas):
@ -45,13 +45,15 @@ def test_decentralized_grant(blockchain_alice, blockchain_bob, blockchain_ursula
policy_id = keccak_digest(label + bytes(blockchain_bob.stamp))
assert policy_id == policy.id
treasure_map = blockchain_bob._try_orient(policy.treasure_map, policy.publisher_verifying_key)
# The number of actually enacted arrangements is exactly equal to n.
assert len(policy.treasure_map.destinations) == n
assert len(treasure_map.destinations) == n
# Let's look at the enacted arrangements.
for ursula in blockchain_ursulas:
if ursula.checksum_address in policy.treasure_map.destinations:
kfrag_kit = policy.treasure_map.destinations[ursula.checksum_address]
if ursula.checksum_address in treasure_map.destinations:
kfrag_kit = treasure_map.destinations[ursula.checksum_address]
# TODO: try to decrypt?
# TODO: Use a new type for EncryptedKFrags?
@ -62,11 +64,11 @@ def test_alice_sets_treasure_map_decentralized(enacted_blockchain_policy, blockc
"""
Same as test_alice_sets_treasure_map except with a blockchain policy.
"""
treasure_map_hrac = enacted_blockchain_policy.treasure_map._hrac
treasure_map_hrac = enacted_blockchain_policy.treasure_map.hrac
found = 0
for node in blockchain_bob.matching_nodes_among(blockchain_alice.known_nodes):
with node.datastore.describe(DatastoreTreasureMap, treasure_map_hrac) as treasure_map_on_node:
assert SignedTreasureMap.from_bytes(treasure_map_on_node.treasure_map) == enacted_blockchain_policy.treasure_map
assert EncryptedTreasureMap.from_bytes(treasure_map_on_node.treasure_map).hrac == enacted_blockchain_policy.hrac
found += 1
assert found
@ -94,4 +96,4 @@ def test_bob_retrieves_treasure_map_from_decentralized_node(enacted_blockchain_p
# Now he'll have better success finding that map.
treasure_map_from_wire = bob.get_treasure_map(blockchain_alice.stamp.as_umbral_pubkey(),
enacted_blockchain_policy.label)
assert enacted_blockchain_policy.treasure_map == treasure_map_from_wire
assert enacted_blockchain_policy.treasure_map.hrac == treasure_map_from_wire.hrac

View File

@ -619,7 +619,8 @@ def test_collect_rewards_integration(click_runner,
handpicked_ursulas={ursula})
# Ensure that the handpicked Ursula was selected for the policy
assert ursula.checksum_address in blockchain_policy.treasure_map.destinations
treasure_map = blockchain_bob._try_orient(blockchain_policy.treasure_map, blockchain_policy.publisher_verifying_key)
assert ursula.checksum_address in treasure_map.destinations
# Bob learns about the new staker and joins the policy
blockchain_bob.start_learning_loop()

View File

@ -20,7 +20,6 @@ from base64 import b64encode, b64decode
import pytest
from nucypher.crypto.umbral_adapter import PublicKey
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.powers import DecryptingPower
from nucypher.network.nodes import Learner
from nucypher.policy.maps import TreasureMap
@ -90,7 +89,7 @@ def test_publish_and_get_treasure_map(blockchain_porter_rpc_controller,
random_bob_encrypting_key = PublicKey.from_bytes(
bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"))
random_hrac = "93a9482bdf3b4f2e9df906a35144ca84"
assert len(bytes.fromhex(random_hrac)) == HRAC_LENGTH
assert len(bytes.fromhex(random_hrac)) == TreasureMap.HRAC_LENGTH
get_treasure_map_params = {
'hrac': random_hrac,
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()

View File

@ -22,7 +22,6 @@ from base64 import b64encode
import pytest
from nucypher.crypto.umbral_adapter import PublicKey
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.powers import DecryptingPower
from nucypher.network.nodes import Learner
from nucypher.policy.maps import TreasureMap
@ -106,7 +105,7 @@ def test_publish_and_get_treasure_map(blockchain_porter_web_controller,
random_bob_encrypting_key = PublicKey.from_bytes(
bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"))
random_hrac = "93a9482bdf3b4f2e9df906a35144ca84"
assert len(bytes.fromhex(random_hrac)) == HRAC_LENGTH
assert len(bytes.fromhex(random_hrac)) == TreasureMap.HRAC_LENGTH
get_treasure_map_params = {
'hrac': random_hrac,
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()

View File

@ -18,7 +18,6 @@
import pytest
from nucypher.crypto.umbral_adapter import PublicKey
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.powers import DecryptingPower
from nucypher.policy.maps import TreasureMap
from tests.utils.middleware import MockRestMiddleware
@ -82,7 +81,7 @@ def test_publish_and_get_treasure_map(blockchain_porter,
random_bob_encrypting_key = PublicKey.from_bytes(
bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"))
random_hrac = bytes.fromhex("93a9482bdf3b4f2e9df906a35144ca84")
assert len(random_hrac) == HRAC_LENGTH
assert len(random_hrac) == TreasureMap.HRAC_LENGTH
blockchain_porter.get_treasure_map(hrac=random_hrac,
bob_encrypting_key=random_bob_encrypting_key)
@ -100,7 +99,7 @@ def test_publish_and_get_treasure_map(blockchain_porter,
enacted_policy.label)
retrieved_treasure_map = blockchain_porter.get_treasure_map(hrac=hrac,
bob_encrypting_key=blockchain_bob_encrypting_key)
assert retrieved_treasure_map == treasure_map
assert retrieved_treasure_map.hrac == treasure_map.hrac
def test_exec_work_order(blockchain_porter,

View File

@ -19,29 +19,18 @@ from base64 import b64encode
import pytest
from nucypher.characters.control.specifications.fields import EncryptedTreasureMap
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.crypto.powers import DecryptingPower
from nucypher.utilities.porter.control.specifications.porter_schema import AlicePublishTreasureMap
def test_alice_publish_treasure_map_schema_blockchain_context_default(enacted_blockchain_policy, blockchain_bob):
def test_alice_publish_treasure_map_schema_blockchain_context(enacted_blockchain_policy, blockchain_bob):
alice_publish_treasure_map_schema = AlicePublishTreasureMap() # default is decentralized
run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema=alice_publish_treasure_map_schema,
enacted_blockchain_policy=enacted_blockchain_policy,
blockchain_bob=blockchain_bob)
def test_alice_publish_treasure_map_schema_blockchain_context_set_false(enacted_blockchain_policy, blockchain_bob):
# since non-federated, schema's context doesn't have to be set, but set it anyway to ensure that setting to
# False still works as expected.
alice_publish_treasure_map_schema = AlicePublishTreasureMap() # default is decentralized
alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = False
run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema=alice_publish_treasure_map_schema,
enacted_blockchain_policy=enacted_blockchain_policy,
blockchain_bob=blockchain_bob)
def run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema, enacted_blockchain_policy, blockchain_bob):
# no args
with pytest.raises(InvalidInputData):
@ -84,9 +73,3 @@ def run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema, enac
response_data = {'published': True}
output = alice_publish_treasure_map_schema.dump(obj=response_data)
assert output == response_data
# setting federated context to True
alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = True
with pytest.raises(InvalidInputData):
# failed because federated treasure map expected, but instead non-federated (blockchain) treasure map provided
alice_publish_treasure_map_schema.load(required_data)

View File

@ -244,6 +244,15 @@ def enacted_federated_policy(idle_federated_policy, federated_ursulas):
return enacted_policy
@pytest.fixture(scope="module")
def federated_treasure_map(enacted_federated_policy, federated_bob):
"""
The unencrypted treasure map corresponding to the one in `enacted_federated_policy`
"""
yield federated_bob._try_orient(enacted_federated_policy.treasure_map,
enacted_federated_policy.publisher_verifying_key)
@pytest.fixture(scope="module")
def idle_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, token_economics):
"""
@ -281,6 +290,15 @@ def enacted_blockchain_policy(idle_blockchain_policy, blockchain_ursulas):
return enacted_policy
@pytest.fixture(scope="module")
def blockchain_treasure_map(enacted_blockchain_policy, blockchain_bob):
"""
The unencrypted treasure map corresponding to the one in `enacted_blockchain_policy`
"""
yield blockchain_bob._try_orient(enacted_blockchain_policy.treasure_map,
enacted_blockchain_policy.publisher_verifying_key)
@pytest.fixture(scope="function")
def random_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, token_economics):
random_label = generate_random_label()

View File

@ -72,10 +72,15 @@ def test_alice_rpc_character_control_grant(alice_rpc_test_client, grant_control_
assert 'jsonrpc' in response.data
def test_bob_rpc_character_control_join_policy(bob_rpc_controller, join_control_request, enacted_federated_policy, federated_bob, federated_ursulas):
def test_bob_rpc_character_control_join_policy(bob_rpc_controller,
join_control_request,
federated_treasure_map,
enacted_federated_policy,
federated_bob,
federated_ursulas):
for ursula in federated_ursulas:
if ursula.checksum_address in enacted_federated_policy.treasure_map.destinations:
if ursula.checksum_address in federated_treasure_map.destinations:
# Simulate passing in a teacher-uri
federated_bob.remember_node(ursula)
break

View File

@ -27,7 +27,7 @@ from click.testing import CliRunner
import nucypher
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import DecryptingPower
from nucypher.policy.maps import TreasureMap
from nucypher.policy.maps import EncryptedTreasureMap
click_runner = CliRunner()
@ -89,8 +89,8 @@ def test_alice_web_character_control_grant(alice_web_controller_test_client, gra
assert 'alice_verifying_key' in response_data['result']
map_bytes = b64decode(response_data['result']['treasure_map'])
encrypted_map = TreasureMap.from_bytes(map_bytes)
assert encrypted_map._hrac is not None
encrypted_map = EncryptedTreasureMap.from_bytes(map_bytes)
assert encrypted_map.hrac is not None
# Send bad data to assert error returns
response = alice_web_controller_test_client.put(endpoint, data=json.dumps({'bad': 'input'}))
@ -169,14 +169,18 @@ def test_alice_character_control_decrypt(alice_web_controller_test_client,
assert response.status_code == 405
def test_bob_character_control_join_policy(bob_web_controller_test_client, federated_bob, federated_ursulas, enacted_federated_policy):
def test_bob_character_control_join_policy(bob_web_controller_test_client,
federated_bob,
federated_ursulas,
federated_treasure_map,
enacted_federated_policy):
request_data = {
'label': enacted_federated_policy.label.decode(),
'publisher_verifying_key': bytes(enacted_federated_policy.publisher_verifying_key).hex(),
}
for ursula in federated_ursulas:
if ursula.checksum_address in enacted_federated_policy.treasure_map.destinations:
if ursula.checksum_address in federated_treasure_map.destinations:
# Simulate passing in a teacher-uri
federated_bob.remember_node(ursula)
break

View File

@ -30,20 +30,19 @@ from nucypher.policy.maps import AuthorizedKeyFrag
from tests.utils.middleware import MockRestMiddleware, NodeIsDownMiddleware
def test_bob_cannot_follow_the_treasure_map_in_isolation(enacted_federated_policy, federated_bob):
def test_bob_cannot_follow_the_treasure_map_in_isolation(federated_treasure_map, federated_bob):
# Assume for the moment that Bob has already received a TreasureMap, perhaps via a side channel.
hrac, treasure_map = enacted_federated_policy.hrac, enacted_federated_policy.treasure_map
# Bob knows of no Ursulas.
assert len(federated_bob.known_nodes) == 0
# He can't successfully follow the TreasureMap until he learns of a node to ask.
unknown, known = federated_bob.peek_at_treasure_map(treasure_map=treasure_map)
unknown, known = federated_bob.peek_at_treasure_map(treasure_map=federated_treasure_map)
assert len(known) == 0
# TODO: Show that even with learning loop going, nothing happens here.
# Probably use Clock?
federated_bob.follow_treasure_map(treasure_map=treasure_map)
federated_bob.follow_treasure_map(treasure_map=federated_treasure_map)
assert len(known) == 0
@ -67,11 +66,11 @@ def test_bob_already_knows_all_nodes_in_treasure_map(enacted_federated_policy,
assert len(unknown) == 0
# ...because he already knew of all the Ursulas on the map.
assert len(known) == len(enacted_federated_policy.treasure_map)
assert len(known) == enacted_federated_policy.n
@pytest_twisted.inlineCallbacks
def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_federated_policy,
def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(federated_treasure_map,
federated_ursulas,
certificates_tempdir):
"""
@ -90,7 +89,6 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
federated_only=True)
# Again, let's assume that he received the TreasureMap via a side channel.
hrac, treasure_map = enacted_federated_policy.hrac, enacted_federated_policy.treasure_map
# Now, let's create a scenario in which Bob knows of only one node.
assert len(bob.known_nodes) == 0
@ -99,13 +97,13 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
assert len(bob.known_nodes) == 1
# This time, when he follows the TreasureMap...
unknown_nodes, known_nodes = bob.peek_at_treasure_map(treasure_map=treasure_map)
unknown_nodes, known_nodes = bob.peek_at_treasure_map(treasure_map=federated_treasure_map)
# Bob already knew about one node; the rest are unknown.
assert len(unknown_nodes) == len(treasure_map) - 1
assert len(unknown_nodes) == len(federated_treasure_map) - 1
# He needs to actually follow the treasure map to get the rest.
bob.follow_treasure_map(treasure_map=treasure_map)
bob.follow_treasure_map(treasure_map=federated_treasure_map)
# The nodes in the learning loop are now his top target, but he's not learning yet.
assert not bob._learning_task.running
@ -121,11 +119,12 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
yield d
# ...and he now has no more unknown_nodes.
assert len(bob.known_nodes) == len(treasure_map)
assert len(bob.known_nodes) == len(federated_treasure_map)
bob.disenchant()
def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_policy,
federated_treasure_map,
federated_bob,
federated_alice,
federated_ursulas,
@ -139,10 +138,9 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic
"""
# We pick up our story with Bob already having followed the treasure map above, ie:
hrac, treasure_map = enacted_federated_policy.hrac, enacted_federated_policy.treasure_map
federated_bob.start_learning_loop()
federated_bob.follow_treasure_map(treasure_map=treasure_map, block=True, timeout=1)
federated_bob.follow_treasure_map(treasure_map=federated_treasure_map, block=True, timeout=1)
assert len(federated_bob.known_nodes) == len(federated_ursulas)
@ -158,7 +156,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic
work_orders, _ = federated_bob.work_orders_for_capsules(
message_kit.capsule,
label=enacted_federated_policy.label,
treasure_map=treasure_map,
treasure_map=federated_treasure_map,
alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
num_ursulas=1)
@ -172,7 +170,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic
retained_work_orders, _ = federated_bob.work_orders_for_capsules(
message_kit.capsule,
label=enacted_federated_policy.label,
treasure_map=treasure_map,
treasure_map=federated_treasure_map,
alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
num_ursulas=1)
@ -206,7 +204,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic
raise RuntimeError("We've lost track of the Ursula that has the WorkOrder. Can't really proceed.")
# Ursula decrypts the encrypted KFrag
encrypted_kfrag = enacted_federated_policy.treasure_map.destinations[ursula.checksum_address]
encrypted_kfrag = federated_treasure_map.destinations[ursula.checksum_address]
alice = Alice.from_public_keys(verifying_key=federated_alice.stamp.as_umbral_pubkey())
plaintext_kfrag_payload = ursula.verify_from(stranger=alice,
message_kit=encrypted_kfrag,
@ -227,6 +225,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic
def test_bob_can_use_cfrag_attached_to_completed_workorder(enacted_federated_policy,
federated_treasure_map,
federated_alice,
federated_bob,
federated_ursulas,
@ -243,7 +242,7 @@ def test_bob_can_use_cfrag_attached_to_completed_workorder(enacted_federated_pol
incomplete_work_orders, complete_work_orders = federated_bob.work_orders_for_capsules(
last_capsule_on_side_channel,
label=enacted_federated_policy.label,
treasure_map=enacted_federated_policy.treasure_map,
treasure_map=federated_treasure_map,
alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
num_ursulas=1,
)
@ -262,9 +261,12 @@ def test_bob_can_use_cfrag_attached_to_completed_workorder(enacted_federated_pol
federated_bob._reencrypt(new_work_order, message_kit_dict)
def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_federated_policy, federated_alice,
def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_federated_policy,
federated_treasure_map,
federated_alice,
federated_bob,
federated_ursulas, capsule_side_channel):
federated_ursulas,
capsule_side_channel):
# In our last episode, Bob made a single WorkOrder...
work_orders = list(federated_bob._completed_work_orders.by_ursula.values())
assert len(work_orders) == 1
@ -293,7 +295,7 @@ def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_feder
incomplete_work_orders, complete_work_orders = federated_bob.work_orders_for_capsules(
last_capsule_on_side_channel,
label=enacted_federated_policy.label,
treasure_map=enacted_federated_policy.treasure_map,
treasure_map=federated_treasure_map,
alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
num_ursulas=1)
id_of_this_new_ursula, new_work_order = list(incomplete_work_orders.items())[0]
@ -321,7 +323,7 @@ def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_feder
last_message_kit_on_side_channel.attach_cfrag(new_cfrag)
def test_bob_gathers_and_combines(enacted_federated_policy, federated_bob, federated_alice, capsule_side_channel):
def test_bob_gathers_and_combines(enacted_federated_policy, federated_treasure_map, federated_bob, federated_alice, capsule_side_channel):
# The side channel delivers all that Bob needs at this point:
# - A single MessageKit, containing a Capsule
# - A representation of the data source
@ -331,13 +333,13 @@ def test_bob_gathers_and_combines(enacted_federated_policy, federated_bob, feder
assert len(federated_bob._completed_work_orders) == 2
# ...but the policy requires us to collect more cfrags.
assert len(federated_bob._completed_work_orders) < enacted_federated_policy.treasure_map.m
assert len(federated_bob._completed_work_orders) < federated_treasure_map.m
# Bob can't decrypt yet with just two CFrags. He needs to gather at least m.
with pytest.raises(DecryptingKeypair.DecryptionFailed):
federated_bob.decrypt(the_message_kit)
number_left_to_collect = enacted_federated_policy.treasure_map.m - len(federated_bob._completed_work_orders)
number_left_to_collect = federated_treasure_map.m - len(federated_bob._completed_work_orders)
the_message_kit.set_correctness_keys(
delegating=the_data_source.policy_pubkey,
@ -347,7 +349,7 @@ def test_bob_gathers_and_combines(enacted_federated_policy, federated_bob, feder
new_incomplete_work_orders, _ = federated_bob.work_orders_for_capsules(
the_message_kit.capsule,
label=enacted_federated_policy.label,
treasure_map=enacted_federated_policy.treasure_map,
treasure_map=federated_treasure_map,
alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
num_ursulas=number_left_to_collect)
_id_of_yet_another_ursula, new_work_order = list(new_incomplete_work_orders.items())[0]

View File

@ -35,10 +35,10 @@ def test_federated_bob_full_retrieve_flow(federated_ursulas,
federated_bob,
federated_alice,
capsule_side_channel,
federated_treasure_map,
enacted_federated_policy):
# Assume for the moment that Bob has already received a TreasureMap.
treasure_map = enacted_federated_policy.treasure_map
federated_bob.treasure_maps[treasure_map._hrac] = treasure_map
federated_bob.treasure_maps[federated_treasure_map.hrac] = federated_treasure_map
for ursula in federated_ursulas:
federated_bob.remember_node(ursula)
@ -98,12 +98,8 @@ def test_bob_joins_policy_and_retrieves(federated_alice,
bob.join_policy(label=label,
publisher_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
block=True)
except policy.treasure_map.NowhereToBeFound:
maps = []
for ursula in federated_ursulas:
for map in ursula.treasure_maps.values():
maps.append(map)
if policy.treasure_map in maps:
except TreasureMap.NowhereToBeFound:
if policy.treasure_map.hrac in ursula.treasure_maps:
# This is a nice place to put a breakpoint to examine Bob's failure to join a policy.
bob.join_policy(label=label,
publisher_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
@ -173,29 +169,6 @@ def test_bob_joins_policy_and_retrieves(federated_alice,
bob.disenchant()
def test_treasure_map_serialization(enacted_federated_policy, federated_alice, federated_bob):
treasure_map = enacted_federated_policy.treasure_map
assert treasure_map.m is not None
assert treasure_map.m != NO_DECRYPTION_PERFORMED
assert treasure_map.m == MOCK_POLICY_DEFAULT_M, 'm value is not correct'
serialized_map = bytes(treasure_map)
deserialized_map = TreasureMap.from_bytes(serialized_map)
assert deserialized_map._hrac == treasure_map._hrac
# TreasureMap is currently encrypted
with pytest.raises(TypeError):
deserialized_map.m
with pytest.raises(TypeError):
deserialized_map.destinations
compass = federated_bob.make_compass_for_alice(federated_alice)
deserialized_map.orient(compass)
assert deserialized_map.m == treasure_map.m
assert deserialized_map.destinations == treasure_map.destinations
def test_bob_retrieves_with_treasure_map(
federated_bob, federated_ursulas,
enacted_federated_policy, capsule_side_channel):

View File

@ -21,6 +21,7 @@ import datetime
import maya
import pytest
from nucypher.crypto.kits import PolicyMessageKit
from nucypher.characters.lawful import Enrico
from nucypher.crypto.utils import keccak_digest
from nucypher.policy.orders import Revocation
@ -43,13 +44,19 @@ def test_federated_grant(federated_alice, federated_bob, federated_ursulas):
assert policy_id in federated_alice.active_policies
assert federated_alice.active_policies[policy_id] == policy
treasure_map = federated_bob._try_orient(policy.treasure_map, policy.publisher_verifying_key)
# The number of actually enacted arrangements is exactly equal to n.
assert len(policy.treasure_map.destinations) == n
assert len(treasure_map.destinations) == n
# Let's look at the enacted arrangements.
for ursula in federated_ursulas:
if ursula.checksum_address in policy.treasure_map.destinations:
assert ursula.checksum_address in policy.treasure_map.destinations
if ursula.checksum_address in treasure_map.destinations:
kfrag_kit = treasure_map.destinations[ursula.checksum_address]
# TODO: try to decrypt?
# TODO: Use a new type for EncryptedKFrags?
assert isinstance(kfrag_kit, PolicyMessageKit)
def test_federated_alice_can_decrypt(federated_alice, federated_bob):

View File

@ -73,7 +73,7 @@ def test_treasuremap_validation(enacted_federated_policy):
"""Tell people exactly what's wrong with their treasuremaps"""
class TreasureMapsOnly(BaseSchema):
tmap = EncryptedTreasureMap(federated_only=True)
tmap = EncryptedTreasureMap()
# this will raise a base64 error
with pytest.raises(SpecificationError) as e:
@ -87,8 +87,8 @@ def test_treasuremap_validation(enacted_federated_policy):
with pytest.raises(InvalidInputData) as e:
TreasureMapsOnly().load({'tmap': "VGhpcyBpcyB0b3RhbGx5IG5vdCBhIHRyZWFzdXJlbWFwLg=="})
assert "Could not parse tmap" in str(e)
assert "Can't split a message with more bytes than the original splittable" in str(e)
assert "Could not convert input for tmap" in str(e)
assert "Invalid treasure map contents" in str(e)
# a valid treasuremap for once...
tmap_bytes = bytes(enacted_federated_policy.treasure_map)

View File

@ -87,7 +87,7 @@ def test_alice_can_grant_even_when_the_first_nodes_she_tries_are_down(federated_
# policy = alice_grant_action()
# The number of actually enacted arrangements is exactly equal to n.
assert len(policy.treasure_map.destinations) == n
assert policy.n == n
def test_node_has_changed_cert(federated_alice, federated_ursulas):

View File

@ -21,7 +21,7 @@ import pytest
from nucypher.characters.lawful import Ursula
from nucypher.crypto.utils import keccak_digest
from nucypher.datastore.models import EncryptedTreasureMap as DatastoreTreasureMap
from nucypher.policy.maps import TreasureMap as FederatedTreasureMap
from nucypher.policy.maps import EncryptedTreasureMap
def test_alice_creates_policy_with_correct_hrac(federated_alice, federated_bob, idle_federated_policy):
@ -37,11 +37,11 @@ def test_alice_sets_treasure_map(federated_alice, federated_bob, enacted_federat
"""
Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and ...... TODO
"""
hrac = enacted_federated_policy.treasure_map._hrac
hrac = enacted_federated_policy.treasure_map.hrac
found = 0
for node in federated_bob.matching_nodes_among(federated_alice.known_nodes):
with node.datastore.describe(DatastoreTreasureMap, hrac) as treasure_map_on_node:
assert FederatedTreasureMap.from_bytes(treasure_map_on_node.treasure_map) == enacted_federated_policy.treasure_map
assert EncryptedTreasureMap.from_bytes(treasure_map_on_node.treasure_map).hrac == enacted_federated_policy.hrac
found += 1
assert found
@ -52,10 +52,10 @@ def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alic
The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it.
"""
hrac = enacted_federated_policy.treasure_map._hrac
hrac = enacted_federated_policy.treasure_map.hrac
an_ursula = federated_bob.matching_nodes_among(federated_ursulas)[0]
with an_ursula.datastore.describe(DatastoreTreasureMap, hrac) as treasure_map_record:
treasure_map_on_network = FederatedTreasureMap.from_bytes(treasure_map_record.treasure_map)
treasure_map_on_network = EncryptedTreasureMap.from_bytes(treasure_map_record.treasure_map)
hrac_by_bob = federated_bob.construct_policy_hrac(federated_alice.stamp, enacted_federated_policy.label)
assert enacted_federated_policy.hrac == hrac_by_bob
@ -90,14 +90,14 @@ def test_bob_can_retrieve_the_treasure_map_and_decrypt_it(federated_alice, feder
treasure_map_from_wire = bob.get_treasure_map(federated_alice.stamp.as_umbral_pubkey(),
enacted_federated_policy.label)
assert enacted_federated_policy.treasure_map == treasure_map_from_wire
assert enacted_federated_policy.hrac == treasure_map_from_wire.hrac
def test_treasure_map_is_legit(federated_bob, enacted_federated_policy):
def test_treasure_map_is_legit(federated_bob, federated_treasure_map, enacted_federated_policy):
"""
Sure, the TreasureMap can get to Bob, but we also need to know that each Ursula in the TreasureMap is on the network.
"""
for ursula_address, _node_id in enacted_federated_policy.treasure_map:
for ursula_address, _node_id in federated_treasure_map:
if ursula_address not in federated_bob.known_nodes.addresses():
pytest.fail(f"Bob didn't know about {ursula_address}")

View File

@ -36,4 +36,7 @@ def random_federated_treasure_map_data(federated_alice, federated_bob, federated
verified_kfrags=kfrags,
m=threshold)
yield federated_bob.public_keys(DecryptingPower), random_treasure_map
enc_treasure_map = random_treasure_map.prepare_for_publication(publisher=federated_alice,
bob=federated_bob)
yield federated_bob.public_keys(DecryptingPower), enc_treasure_map

View File

@ -89,7 +89,7 @@ def test_publish_and_get_treasure_map(federated_porter_rpc_controller,
# ensure that random treasure map cannot be obtained since not available
with pytest.raises(TreasureMap.NowhereToBeFound):
get_treasure_map_params = {
'hrac': random_treasure_map._hrac.hex(),
'hrac': random_treasure_map.hrac.hex(),
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
}
request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params}
@ -106,7 +106,7 @@ def test_publish_and_get_treasure_map(federated_porter_rpc_controller,
# try getting the random treasure map now
get_treasure_map_params = {
'hrac': random_treasure_map._hrac.hex(),
'hrac': random_treasure_map.hrac.hex(),
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
}
request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params}

View File

@ -103,7 +103,7 @@ def test_publish_and_get_treasure_map(federated_porter_web_controller,
# ensure that random treasure map cannot be obtained since not available
with pytest.raises(TreasureMap.NowhereToBeFound):
get_treasure_map_params = {
'hrac': random_treasure_map._hrac.hex(),
'hrac': random_treasure_map.hrac.hex(),
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
}
federated_porter_web_controller.get('/get_treasure_map', data=json.dumps(get_treasure_map_params))
@ -121,7 +121,7 @@ def test_publish_and_get_treasure_map(federated_porter_web_controller,
# try getting the random treasure map now
get_treasure_map_params = {
'hrac': random_treasure_map._hrac.hex(),
'hrac': random_treasure_map.hrac.hex(),
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
}
response = federated_porter_web_controller.get('/get_treasure_map',
@ -206,7 +206,7 @@ def test_endpoints_basic_auth(federated_porter_basic_auth_web_controller,
# /get_treasure_map
get_treasure_map_params = {
'hrac': random_treasure_map._hrac.hex(),
'hrac': random_treasure_map.hrac.hex(),
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
}
response = federated_porter_basic_auth_web_controller.get('/get_treasure_map',

View File

@ -77,7 +77,7 @@ def test_publish_and_get_treasure_map(federated_porter,
# ensure that random treasure map cannot be obtained since not available
with pytest.raises(TreasureMap.NowhereToBeFound):
federated_porter.get_treasure_map(hrac=random_treasure_map._hrac,
federated_porter.get_treasure_map(hrac=random_treasure_map.hrac,
bob_encrypting_key=random_bob_encrypting_key)
# publish the random treasure map
@ -85,16 +85,16 @@ def test_publish_and_get_treasure_map(federated_porter,
bob_encrypting_key=random_bob_encrypting_key)
# try getting the random treasure map now
treasure_map = federated_porter.get_treasure_map(hrac=random_treasure_map._hrac,
treasure_map = federated_porter.get_treasure_map(hrac=random_treasure_map.hrac,
bob_encrypting_key=random_bob_encrypting_key)
assert treasure_map._hrac == random_treasure_map._hrac
assert treasure_map.hrac == random_treasure_map.hrac
# try getting an already existing policy
hrac = federated_bob.construct_policy_hrac(federated_alice.stamp.as_umbral_pubkey(),
enacted_federated_policy.label)
treasure_map = federated_porter.get_treasure_map(hrac=hrac,
bob_encrypting_key=federated_bob.public_keys(DecryptingPower))
assert treasure_map == enacted_federated_policy.treasure_map
assert treasure_map.hrac == enacted_federated_policy.hrac
def test_exec_work_order(federated_porter,

View File

@ -19,7 +19,6 @@ from base64 import b64encode
import pytest
from nucypher.characters.control.specifications.fields import EncryptedTreasureMap
from nucypher.control.specifications.exceptions import InvalidArgumentCombo, InvalidInputData
from nucypher.crypto.powers import DecryptingPower
from nucypher.crypto.umbral_adapter import SecretKey
@ -169,7 +168,6 @@ def test_alice_publish_treasure_map_schema_federated_context(enacted_federated_p
# since federated, schema's context must be set - so create one schema
# and reuse (it doesn't hold state other than the context)
alice_publish_treasure_map_schema = AlicePublishTreasureMap()
alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = True
# no args
with pytest.raises(InvalidInputData):
@ -213,12 +211,6 @@ def test_alice_publish_treasure_map_schema_federated_context(enacted_federated_p
output = alice_publish_treasure_map_schema.dump(obj=response_data)
assert output == response_data
# setting federated context to False fails
alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = False
with pytest.raises(InvalidInputData):
# failed because non-federated (blockchain) treasure map expected, but instead federated treasure map provided
alice_publish_treasure_map_schema.load(required_data)
def test_alice_revoke():
pass # TODO

View File

@ -164,7 +164,7 @@ def test_umbral_signature():
def test_treasure_map(enacted_federated_policy):
treasure_map = enacted_federated_policy.treasure_map
field = EncryptedTreasureMap(federated_only=True)
field = EncryptedTreasureMap()
serialized = field._serialize(value=treasure_map, attr=None, obj=None)
assert serialized == b64encode(bytes(treasure_map)).decode()

View File

@ -30,7 +30,7 @@ from tests.utils.policy import work_order_setup
def test_hrac_field(enacted_federated_policy):
hrac = enacted_federated_policy.treasure_map._hrac
hrac = enacted_federated_policy.treasure_map.hrac
field = HRAC()
serialized = field._serialize(value=hrac, attr=None, obj=None)

View File

@ -20,50 +20,55 @@ import os
import pytest
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.crypto.umbral_adapter import KeyFrag
from nucypher.policy.maps import TreasureMap, AuthorizedKeyFrag
from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap, AuthorizedKeyFrag
def test_complete_treasure_map_journey(federated_alice, federated_bob, federated_ursulas, idle_federated_policy, mocker):
treasure_map = TreasureMap(m=1)
label = "chili con carne 🔥".encode('utf-8')
kfrags = idle_federated_policy.kfrags
ursulas = list(federated_ursulas)[:len(kfrags)]
bob_encrypting_key = federated_bob.public_keys(DecryptingPower)
bob_verifying_key = federated_bob.public_keys(SigningPower)
treasure_map = TreasureMap.construct_by_publisher(publisher=federated_alice,
bob=federated_bob,
label=label,
ursulas=ursulas,
verified_kfrags=kfrags,
m=1)
kfrag = idle_federated_policy.kfrags[0]
make_kfrag_payload_spy = mocker.spy(AuthorizedKeyFrag, '__bytes__')
treasure_map.derive_hrac(publisher_stamp=federated_alice.stamp,
bob_verifying_key=bob_verifying_key,
label="chili con carne 🔥".encode('utf-8'))
encrypted_kfrags = dict()
for ursula in federated_ursulas:
treasure_map.add_kfrag(ursula, kfrag, federated_alice.stamp)
encrypted_kfrags[ursula.checksum_address] = make_kfrag_payload_spy.spy_return
treasure_map.prepare_for_publication(bob_encrypting_key=bob_encrypting_key,
publisher_stamp=federated_alice.stamp)
ursula_rolodex = {u.checksum_address: u for u in federated_ursulas}
ursula_rolodex = {u.checksum_address: u for u in ursulas}
for ursula_address, encrypted_kfrag in treasure_map.destinations.items():
assert ursula_address in ursula_rolodex
ursula = ursula_rolodex[ursula_address]
kfrag_payload = encrypted_kfrags[ursula.checksum_address]
assert kfrag_payload == ursula.verify_from(federated_alice, encrypted_kfrag, decrypt=True) # FIXME: 2203
auth_kfrag_bytes = ursula.verify_from(federated_alice, encrypted_kfrag, decrypt=True) # FIXME: 2203
auth_kfrag = AuthorizedKeyFrag.from_bytes(auth_kfrag_bytes)
ursula.verify_kfrag_authorization(hrac=treasure_map.hrac,
author=federated_alice,
publisher=federated_alice,
authorized_kfrag=auth_kfrag)
serialized_map = bytes(treasure_map)
# ...
deserialized_map = TreasureMap.from_bytes(serialized_map)
compass = federated_bob.make_compass_for_alice(federated_alice)
deserialized_map.orient(compass)
assert treasure_map.destinations == deserialized_map.destinations
assert treasure_map.hrac == deserialized_map.hrac
assert treasure_map.m == deserialized_map.m == 1
assert set(treasure_map.destinations) == set(deserialized_map.destinations)
assert treasure_map == deserialized_map
enc_treasure_map = treasure_map.prepare_for_publication(publisher=federated_alice,
bob=federated_bob)
enc_serialized_map = bytes(enc_treasure_map)
# ...
enc_deserialized_map = EncryptedTreasureMap.from_bytes(enc_serialized_map)
compass = federated_bob.make_compass_for_alice(federated_alice)
decrypted_map = enc_deserialized_map.orient(compass)
assert treasure_map.m == decrypted_map.m == 1
assert treasure_map.destinations == decrypted_map.destinations
assert treasure_map.hrac == decrypted_map.hrac
@pytest.mark.skip(reason='Backwards-incompatible with umbral 0.2+')

View File

@ -42,8 +42,9 @@ def work_order_setup(enacted_policy,
bob,
alice):
treasure_map = bob._try_orient(enacted_policy.treasure_map, enacted_policy.publisher_verifying_key)
# We pick up our story with Bob already having followed the treasure map above, ie:
treasure_map = enacted_policy.treasure_map
bob.start_learning_loop()
bob.follow_treasure_map(treasure_map=treasure_map, block=True, timeout=1)