mirror of https://github.com/nucypher/nucypher.git
Split treasure maps into decrypted and encrypted
parent
5b5cd4bd84
commit
509b8c1bfc
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()}")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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}")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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+')
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue