mirror of https://github.com/nucypher/nucypher.git
Move retrieval machinery to network/retrieval and revocation to policy/revocation
parent
1ad868bf41
commit
a9cc13e825
|
@ -20,9 +20,7 @@ import contextlib
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from collections import OrderedDict, defaultdict, namedtuple
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
@ -84,15 +82,12 @@ from nucypher.crypto.powers import (
|
||||||
TLSHostingPower,
|
TLSHostingPower,
|
||||||
)
|
)
|
||||||
from nucypher.crypto.signing import InvalidSignature
|
from nucypher.crypto.signing import InvalidSignature
|
||||||
from nucypher.crypto.splitters import key_splitter, signature_splitter, cfrag_splitter
|
from nucypher.crypto.splitters import key_splitter, signature_splitter
|
||||||
from nucypher.crypto.umbral_adapter import (
|
from nucypher.crypto.umbral_adapter import (
|
||||||
Capsule,
|
|
||||||
PublicKey,
|
PublicKey,
|
||||||
VerificationError,
|
VerificationError,
|
||||||
reencrypt,
|
reencrypt,
|
||||||
KeyFrag,
|
|
||||||
VerifiedKeyFrag,
|
VerifiedKeyFrag,
|
||||||
Signature
|
|
||||||
)
|
)
|
||||||
from nucypher.datastore.datastore import DatastoreTransactionError, RecordNotFound
|
from nucypher.datastore.datastore import DatastoreTransactionError, RecordNotFound
|
||||||
from nucypher.datastore.queries import find_expired_policies, find_expired_treasure_maps
|
from nucypher.datastore.queries import find_expired_policies, find_expired_treasure_maps
|
||||||
|
@ -101,18 +96,12 @@ from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||||
from nucypher.network.middleware import RestMiddleware
|
from nucypher.network.middleware import RestMiddleware
|
||||||
from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher
|
from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher
|
||||||
from nucypher.network.protocols import InterfaceInfo, parse_node_uri
|
from nucypher.network.protocols import InterfaceInfo, parse_node_uri
|
||||||
|
from nucypher.network.retrieval import RetrievalClient, ReencryptionResponse
|
||||||
from nucypher.network.server import ProxyRESTServer, make_rest_app
|
from nucypher.network.server import ProxyRESTServer, make_rest_app
|
||||||
from nucypher.network.trackers import AvailabilityTracker
|
from nucypher.network.trackers import AvailabilityTracker
|
||||||
from nucypher.policy.hrac import HRAC
|
from nucypher.policy.hrac import HRAC
|
||||||
from nucypher.policy.kits import MessageKit, PolicyMessageKit, RetrievalKit
|
from nucypher.policy.kits import MessageKit, PolicyMessageKit
|
||||||
from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap, AuthorizedKeyFrag
|
from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap, AuthorizedKeyFrag
|
||||||
from nucypher.policy.orders import (
|
|
||||||
RetrievalWorkOrder,
|
|
||||||
ReencryptionRequest,
|
|
||||||
ReencryptionResponse,
|
|
||||||
RetrievalPlan,
|
|
||||||
RetrievalResult,
|
|
||||||
)
|
|
||||||
from nucypher.policy.policies import Policy
|
from nucypher.policy.policies import Policy
|
||||||
from nucypher.utilities.logging import Logger
|
from nucypher.utilities.logging import Logger
|
||||||
from nucypher.utilities.networking import validate_worker_ip
|
from nucypher.utilities.networking import validate_worker_ip
|
||||||
|
@ -745,144 +734,6 @@ class Bob(Character):
|
||||||
return controller
|
return controller
|
||||||
|
|
||||||
|
|
||||||
class RetrievalClient:
|
|
||||||
"""
|
|
||||||
Capsule frag retrieval machinery shared between Bob and Porter.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, learner: Learner):
|
|
||||||
self._learner = learner
|
|
||||||
self.log = Logger(self.__class__.__name__)
|
|
||||||
|
|
||||||
def _request_reencryption(self,
|
|
||||||
ursula: 'Ursula',
|
|
||||||
reencryption_request: 'ReencryptionRequest',
|
|
||||||
delegating_key: PublicKey,
|
|
||||||
receiving_key: PublicKey,
|
|
||||||
) -> Dict['Capsule', 'VerifiedCapsuleFrag']:
|
|
||||||
"""
|
|
||||||
Sends a reencryption request to a single Ursula and processes the results.
|
|
||||||
|
|
||||||
Returns reencrypted capsule frags matched to corresponding capsules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
middleware = self._learner.network_middleware
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = middleware.reencrypt(ursula, bytes(reencryption_request))
|
|
||||||
except NodeSeemsToBeDown as e:
|
|
||||||
# TODO: What to do here? Ursula isn't supposed to be down. NRN
|
|
||||||
message = (f"Ursula ({ursula}) seems to be down "
|
|
||||||
f"while trying to complete ReencryptionRequest: {reencryption_request}")
|
|
||||||
self.log.info(message)
|
|
||||||
raise RuntimeError(message) from e
|
|
||||||
except middleware.NotFound as e:
|
|
||||||
# This Ursula claims not to have a matching KFrag. Maybe this has been revoked?
|
|
||||||
# TODO: What's the thing to do here?
|
|
||||||
# Do we want to track these Ursulas in some way in case they're lying? #567
|
|
||||||
message = (f"Ursula ({ursula}) claims not to have the KFrag "
|
|
||||||
f"to complete ReencryptionRequest: {reencryption_request}. "
|
|
||||||
f"Has access been revoked?")
|
|
||||||
self.log.warn(message)
|
|
||||||
raise RuntimeError(message) from e
|
|
||||||
except middleware.UnexpectedResponse:
|
|
||||||
raise # TODO: Handle this
|
|
||||||
|
|
||||||
try:
|
|
||||||
reencryption_response = ReencryptionResponse.from_bytes(response.content)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Ursula ({ursula}) returned an invalid response: {e}."
|
|
||||||
self.log.warn(message)
|
|
||||||
raise RuntimeError(message)
|
|
||||||
|
|
||||||
if len(reencryption_request.capsules) != len(reencryption_response.cfrags):
|
|
||||||
message = (f"Ursula ({ursula}) gave back the wrong number of cfrags. "
|
|
||||||
"She's up to something.")
|
|
||||||
self.log.warn(message)
|
|
||||||
raise RuntimeError(message)
|
|
||||||
|
|
||||||
ursula_verifying_key = ursula.stamp.as_umbral_pubkey()
|
|
||||||
capsules_bytes = b''.join(bytes(capsule) for capsule in reencryption_request.capsules)
|
|
||||||
cfrags_bytes = b''.join(bytes(cfrag) for cfrag in reencryption_response.cfrags)
|
|
||||||
|
|
||||||
# Validate re-encryption signature
|
|
||||||
if not reencryption_response.signature.verify(ursula_verifying_key, capsules_bytes + cfrags_bytes):
|
|
||||||
message = (f"{reencryption_request.capsules} and {reencryption_response.cfrags} "
|
|
||||||
"are not properly signed by Ursula.")
|
|
||||||
self.log.warn(message)
|
|
||||||
# TODO: Instead of raising, we should do something (#957)
|
|
||||||
raise InvalidSignature(message)
|
|
||||||
|
|
||||||
verified_cfrags = {}
|
|
||||||
|
|
||||||
for capsule, cfrag in zip(reencryption_request.capsules, reencryption_response.cfrags):
|
|
||||||
|
|
||||||
# TODO: should we allow partially valid responses?
|
|
||||||
|
|
||||||
# Verify cfrags
|
|
||||||
try:
|
|
||||||
verified_cfrag = cfrag.verify(capsule,
|
|
||||||
verifying_pk=reencryption_request._alice_verifying_key,
|
|
||||||
delegating_pk=delegating_key,
|
|
||||||
receiving_pk=receiving_key)
|
|
||||||
except VerificationError:
|
|
||||||
# In future we may want to remember this Ursula and do something about it
|
|
||||||
raise
|
|
||||||
|
|
||||||
verified_cfrags[capsule] = verified_cfrag
|
|
||||||
|
|
||||||
return verified_cfrags
|
|
||||||
|
|
||||||
def retrieve_cfrags(
|
|
||||||
self,
|
|
||||||
treasure_map: TreasureMap,
|
|
||||||
retrieval_kits: Sequence[RetrievalKit],
|
|
||||||
alice_verifying_key: PublicKey, # KeyFrag signer's key
|
|
||||||
bob_encrypting_key: PublicKey, # User's public key (reencryption target)
|
|
||||||
bob_verifying_key: PublicKey,
|
|
||||||
policy_encrypting_key: PublicKey, # Key used to create the policy
|
|
||||||
) -> List[RetrievalResult]:
|
|
||||||
|
|
||||||
# TODO: why is it here? This code shouldn't know about these details.
|
|
||||||
# OK, so we're going to need to do some network activity for this retrieval.
|
|
||||||
# Let's make sure we've seeded.
|
|
||||||
if not self._learner.done_seeding:
|
|
||||||
self._learner.learn_from_teacher_node()
|
|
||||||
|
|
||||||
retrieval_plan = RetrievalPlan(treasure_map=treasure_map, retrieval_kits=retrieval_kits)
|
|
||||||
|
|
||||||
while not retrieval_plan.is_complete():
|
|
||||||
# TODO (#2789): Currently we'll only query one Ursula once during the retrieval.
|
|
||||||
# Alternatively we may re-query Ursulas that were offline until the timeout expires.
|
|
||||||
|
|
||||||
work_order = retrieval_plan.get_work_order()
|
|
||||||
|
|
||||||
if work_order.ursula_address not in self._learner.known_nodes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ursula = self._learner.known_nodes[work_order.ursula_address]
|
|
||||||
reencryption_request = ReencryptionRequest.from_work_order(
|
|
||||||
work_order=work_order,
|
|
||||||
treasure_map=treasure_map,
|
|
||||||
alice_verifying_key=alice_verifying_key,
|
|
||||||
bob_verifying_key=bob_verifying_key)
|
|
||||||
|
|
||||||
try:
|
|
||||||
cfrags = self._request_reencryption(ursula=ursula,
|
|
||||||
reencryption_request=reencryption_request,
|
|
||||||
delegating_key=policy_encrypting_key,
|
|
||||||
receiving_key=bob_encrypting_key)
|
|
||||||
except Exception as e:
|
|
||||||
# TODO (#2789): at this point we can separate the exceptions to "acceptable"
|
|
||||||
# (Ursula is not reachable) and "unacceptable" (Ursula provided bad results).
|
|
||||||
self.log.warn(f"Ursula {ursula} failed to reencrypt: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
retrieval_plan.update(work_order, cfrags)
|
|
||||||
|
|
||||||
return retrieval_plan.results()
|
|
||||||
|
|
||||||
|
|
||||||
class Ursula(Teacher, Character, Worker):
|
class Ursula(Teacher, Character, Worker):
|
||||||
|
|
||||||
banner = URSULA_BANNER
|
banner = URSULA_BANNER
|
||||||
|
|
|
@ -15,24 +15,20 @@ You should have received a copy of the GNU Affero General Public License
|
||||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
from collections import OrderedDict, defaultdict
|
|
||||||
import random
|
import random
|
||||||
from typing import Optional, Dict, Sequence, Union, List, Set
|
from typing import Dict, Sequence, List
|
||||||
|
|
||||||
import maya
|
|
||||||
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
||||||
from constant_sorrow.constants import CFRAG_NOT_RETAINED
|
|
||||||
from eth_typing.evm import ChecksumAddress
|
from eth_typing.evm import ChecksumAddress
|
||||||
from eth_utils.address import to_canonical_address, to_checksum_address
|
from twisted.logger import Logger
|
||||||
|
|
||||||
from nucypher.crypto.signing import SignatureStamp, InvalidSignature
|
from nucypher.crypto.signing import SignatureStamp, InvalidSignature
|
||||||
from nucypher.crypto.splitters import (
|
from nucypher.crypto.splitters import (
|
||||||
key_splitter,
|
|
||||||
capsule_splitter,
|
capsule_splitter,
|
||||||
cfrag_splitter,
|
cfrag_splitter,
|
||||||
|
key_splitter,
|
||||||
signature_splitter,
|
signature_splitter,
|
||||||
kfrag_splitter
|
|
||||||
)
|
)
|
||||||
from nucypher.crypto.umbral_adapter import (
|
from nucypher.crypto.umbral_adapter import (
|
||||||
Capsule,
|
Capsule,
|
||||||
|
@ -40,10 +36,13 @@ from nucypher.crypto.umbral_adapter import (
|
||||||
PublicKey,
|
PublicKey,
|
||||||
Signature,
|
Signature,
|
||||||
VerifiedCapsuleFrag,
|
VerifiedCapsuleFrag,
|
||||||
|
VerificationError,
|
||||||
)
|
)
|
||||||
|
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||||
|
from nucypher.network.nodes import Learner
|
||||||
from nucypher.policy.hrac import HRAC, hrac_splitter
|
from nucypher.policy.hrac import HRAC, hrac_splitter
|
||||||
from nucypher.policy.kits import MessageKit
|
from nucypher.policy.kits import MessageKit, RetrievalKit, RetrievalResult
|
||||||
from nucypher.policy.maps import AuthorizedKeyFrag, TreasureMap
|
from nucypher.policy.maps import TreasureMap
|
||||||
|
|
||||||
|
|
||||||
class RetrievalPlan:
|
class RetrievalPlan:
|
||||||
|
@ -52,7 +51,7 @@ class RetrievalPlan:
|
||||||
during retrieval.
|
during retrieval.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, treasure_map: TreasureMap, retrieval_kits: Sequence['RetrievalKit']):
|
def __init__(self, treasure_map: TreasureMap, retrieval_kits: Sequence[RetrievalKit]):
|
||||||
|
|
||||||
# Record the retrieval kits order
|
# Record the retrieval kits order
|
||||||
self._capsules = [retrieval_kit.capsule for retrieval_kit in retrieval_kits]
|
self._capsules = [retrieval_kit.capsule for retrieval_kit in retrieval_kits]
|
||||||
|
@ -129,36 +128,6 @@ class RetrievalPlan:
|
||||||
return [RetrievalResult(self._results[capsule]) for capsule in self._capsules]
|
return [RetrievalResult(self._results[capsule]) for capsule in self._capsules]
|
||||||
|
|
||||||
|
|
||||||
class RetrievalResult:
|
|
||||||
"""
|
|
||||||
An object representing retrieval results for a single capsule.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def empty(cls):
|
|
||||||
return cls({})
|
|
||||||
|
|
||||||
def __init__(self, cfrags: Dict[ChecksumAddress, VerifiedCapsuleFrag]):
|
|
||||||
self.cfrags = cfrags
|
|
||||||
|
|
||||||
def addresses(self) -> Set[ChecksumAddress]:
|
|
||||||
return set(self.cfrags)
|
|
||||||
|
|
||||||
def with_result(self, result: 'RetrievalResult') -> 'RetrievalResult':
|
|
||||||
"""
|
|
||||||
Joins two RetrievalResult objects.
|
|
||||||
|
|
||||||
If both objects contain cfrags from the same Ursula,
|
|
||||||
the one from `result` will be kept.
|
|
||||||
"""
|
|
||||||
# TODO: would `+` or `|` operator be more suitable here?
|
|
||||||
|
|
||||||
# TODO: check for overlap?
|
|
||||||
new_cfrags = dict(self.cfrags)
|
|
||||||
new_cfrags.update(result.cfrags)
|
|
||||||
return RetrievalResult(cfrags=new_cfrags)
|
|
||||||
|
|
||||||
|
|
||||||
class RetrievalWorkOrder:
|
class RetrievalWorkOrder:
|
||||||
"""
|
"""
|
||||||
A work order issued by a retrieval plan to request reencryption from an Ursula
|
A work order issued by a retrieval plan to request reencryption from an Ursula
|
||||||
|
@ -273,69 +242,139 @@ class ReencryptionResponse:
|
||||||
return bytes(self.signature) + b''.join(bytes(cfrag) for cfrag in self.cfrags)
|
return bytes(self.signature) + b''.join(bytes(cfrag) for cfrag in self.cfrags)
|
||||||
|
|
||||||
|
|
||||||
|
class RetrievalClient:
|
||||||
class Revocation:
|
|
||||||
"""
|
"""
|
||||||
Represents a string used by characters to perform a revocation on a specific
|
Capsule frag retrieval machinery shared between Bob and Porter.
|
||||||
Ursula. It's a bytestring made of the following format:
|
|
||||||
REVOKE-<arrangement id to revoke><signature of the previous string>
|
|
||||||
This is sent as a payload in a DELETE method to the /KFrag/ endpoint.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PREFIX = b'REVOKE-'
|
def __init__(self, learner: Learner):
|
||||||
revocation_splitter = BytestringSplitter(
|
self._learner = learner
|
||||||
(bytes, len(PREFIX)),
|
self.log = Logger(self.__class__.__name__)
|
||||||
(bytes, 20), # ursula canonical address
|
|
||||||
(bytes, AuthorizedKeyFrag.ENCRYPTED_SIZE), # encrypted kfrag payload (includes writ)
|
|
||||||
signature_splitter
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self,
|
def _request_reencryption(self,
|
||||||
ursula_checksum_address: ChecksumAddress, # TODO: Use staker address instead (what if the staker rebonds)?
|
ursula: 'Ursula',
|
||||||
encrypted_kfrag: bytes,
|
reencryption_request: 'ReencryptionRequest',
|
||||||
signer: 'SignatureStamp' = None,
|
delegating_key: PublicKey,
|
||||||
signature: Signature = None):
|
receiving_key: PublicKey,
|
||||||
|
) -> Dict['Capsule', 'VerifiedCapsuleFrag']:
|
||||||
self.ursula_checksum_address = ursula_checksum_address
|
|
||||||
self.encrypted_kfrag = encrypted_kfrag
|
|
||||||
|
|
||||||
if not (bool(signer) ^ bool(signature)):
|
|
||||||
raise ValueError("Either pass a signer or a signature; not both.")
|
|
||||||
elif signer:
|
|
||||||
self.signature = signer(self.payload)
|
|
||||||
elif signature:
|
|
||||||
self.signature = signature
|
|
||||||
|
|
||||||
def __bytes__(self):
|
|
||||||
return self.payload + bytes(self.signature)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return bytes(self)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(bytes(self))
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return bytes(self) == bytes(other)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def payload(self):
|
|
||||||
return self.PREFIX \
|
|
||||||
+ to_canonical_address(self.ursula_checksum_address) \
|
|
||||||
+ bytes(self.encrypted_kfrag) \
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_bytes(cls, revocation_bytes):
|
|
||||||
prefix, ursula_canonical_address, ekfrag, signature = cls.revocation_splitter(revocation_bytes)
|
|
||||||
ursula_checksum_address = to_checksum_address(ursula_canonical_address)
|
|
||||||
return cls(ursula_checksum_address=ursula_checksum_address,
|
|
||||||
encrypted_kfrag=ekfrag,
|
|
||||||
signature=signature)
|
|
||||||
|
|
||||||
def verify_signature(self, alice_verifying_key: 'PublicKey') -> bool:
|
|
||||||
"""
|
"""
|
||||||
Verifies the revocation was from the provided pubkey.
|
Sends a reencryption request to a single Ursula and processes the results.
|
||||||
|
|
||||||
|
Returns reencrypted capsule frags matched to corresponding capsules.
|
||||||
"""
|
"""
|
||||||
if not self.signature.verify(self.payload, alice_verifying_key):
|
|
||||||
raise InvalidSignature(f"Revocation has an invalid signature: {self.signature}")
|
middleware = self._learner.network_middleware
|
||||||
return True
|
|
||||||
|
try:
|
||||||
|
response = middleware.reencrypt(ursula, bytes(reencryption_request))
|
||||||
|
except NodeSeemsToBeDown as e:
|
||||||
|
# TODO: What to do here? Ursula isn't supposed to be down. NRN
|
||||||
|
message = (f"Ursula ({ursula}) seems to be down "
|
||||||
|
f"while trying to complete ReencryptionRequest: {reencryption_request}")
|
||||||
|
self.log.info(message)
|
||||||
|
raise RuntimeError(message) from e
|
||||||
|
except middleware.NotFound as e:
|
||||||
|
# This Ursula claims not to have a matching KFrag. Maybe this has been revoked?
|
||||||
|
# TODO: What's the thing to do here?
|
||||||
|
# Do we want to track these Ursulas in some way in case they're lying? #567
|
||||||
|
message = (f"Ursula ({ursula}) claims not to have the KFrag "
|
||||||
|
f"to complete ReencryptionRequest: {reencryption_request}. "
|
||||||
|
f"Has access been revoked?")
|
||||||
|
self.log.warn(message)
|
||||||
|
raise RuntimeError(message) from e
|
||||||
|
except middleware.UnexpectedResponse:
|
||||||
|
raise # TODO: Handle this
|
||||||
|
|
||||||
|
try:
|
||||||
|
reencryption_response = ReencryptionResponse.from_bytes(response.content)
|
||||||
|
except Exception as e:
|
||||||
|
message = f"Ursula ({ursula}) returned an invalid response: {e}."
|
||||||
|
self.log.warn(message)
|
||||||
|
raise RuntimeError(message)
|
||||||
|
|
||||||
|
if len(reencryption_request.capsules) != len(reencryption_response.cfrags):
|
||||||
|
message = (f"Ursula ({ursula}) gave back the wrong number of cfrags. "
|
||||||
|
"She's up to something.")
|
||||||
|
self.log.warn(message)
|
||||||
|
raise RuntimeError(message)
|
||||||
|
|
||||||
|
ursula_verifying_key = ursula.stamp.as_umbral_pubkey()
|
||||||
|
capsules_bytes = b''.join(bytes(capsule) for capsule in reencryption_request.capsules)
|
||||||
|
cfrags_bytes = b''.join(bytes(cfrag) for cfrag in reencryption_response.cfrags)
|
||||||
|
|
||||||
|
# Validate re-encryption signature
|
||||||
|
if not reencryption_response.signature.verify(ursula_verifying_key, capsules_bytes + cfrags_bytes):
|
||||||
|
message = (f"{reencryption_request.capsules} and {reencryption_response.cfrags} "
|
||||||
|
"are not properly signed by Ursula.")
|
||||||
|
self.log.warn(message)
|
||||||
|
# TODO: Instead of raising, we should do something (#957)
|
||||||
|
raise InvalidSignature(message)
|
||||||
|
|
||||||
|
verified_cfrags = {}
|
||||||
|
|
||||||
|
for capsule, cfrag in zip(reencryption_request.capsules, reencryption_response.cfrags):
|
||||||
|
|
||||||
|
# TODO: should we allow partially valid responses?
|
||||||
|
|
||||||
|
# Verify cfrags
|
||||||
|
try:
|
||||||
|
verified_cfrag = cfrag.verify(capsule,
|
||||||
|
verifying_pk=reencryption_request._alice_verifying_key,
|
||||||
|
delegating_pk=delegating_key,
|
||||||
|
receiving_pk=receiving_key)
|
||||||
|
except VerificationError:
|
||||||
|
# In future we may want to remember this Ursula and do something about it
|
||||||
|
raise
|
||||||
|
|
||||||
|
verified_cfrags[capsule] = verified_cfrag
|
||||||
|
|
||||||
|
return verified_cfrags
|
||||||
|
|
||||||
|
def retrieve_cfrags(
|
||||||
|
self,
|
||||||
|
treasure_map: TreasureMap,
|
||||||
|
retrieval_kits: Sequence[RetrievalKit],
|
||||||
|
alice_verifying_key: PublicKey, # KeyFrag signer's key
|
||||||
|
bob_encrypting_key: PublicKey, # User's public key (reencryption target)
|
||||||
|
bob_verifying_key: PublicKey,
|
||||||
|
policy_encrypting_key: PublicKey, # Key used to create the policy
|
||||||
|
) -> List[RetrievalResult]:
|
||||||
|
|
||||||
|
# TODO: why is it here? This code shouldn't know about these details.
|
||||||
|
# OK, so we're going to need to do some network activity for this retrieval.
|
||||||
|
# Let's make sure we've seeded.
|
||||||
|
if not self._learner.done_seeding:
|
||||||
|
self._learner.learn_from_teacher_node()
|
||||||
|
|
||||||
|
retrieval_plan = RetrievalPlan(treasure_map=treasure_map, retrieval_kits=retrieval_kits)
|
||||||
|
|
||||||
|
while not retrieval_plan.is_complete():
|
||||||
|
# TODO (#2789): Currently we'll only query one Ursula once during the retrieval.
|
||||||
|
# Alternatively we may re-query Ursulas that were offline until the timeout expires.
|
||||||
|
|
||||||
|
work_order = retrieval_plan.get_work_order()
|
||||||
|
|
||||||
|
if work_order.ursula_address not in self._learner.known_nodes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ursula = self._learner.known_nodes[work_order.ursula_address]
|
||||||
|
reencryption_request = ReencryptionRequest.from_work_order(
|
||||||
|
work_order=work_order,
|
||||||
|
treasure_map=treasure_map,
|
||||||
|
alice_verifying_key=alice_verifying_key,
|
||||||
|
bob_verifying_key=bob_verifying_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfrags = self._request_reencryption(ursula=ursula,
|
||||||
|
reencryption_request=reencryption_request,
|
||||||
|
delegating_key=policy_encrypting_key,
|
||||||
|
receiving_key=bob_encrypting_key)
|
||||||
|
except Exception as e:
|
||||||
|
# TODO (#2789): at this point we can separate the exceptions to "acceptable"
|
||||||
|
# (Ursula is not reachable) and "unacceptable" (Ursula provided bad results).
|
||||||
|
self.log.warn(f"Ursula {ursula} failed to reencrypt: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
retrieval_plan.update(work_order, cfrags)
|
||||||
|
|
||||||
|
return retrieval_plan.results()
|
|
@ -34,12 +34,13 @@ from nucypher.crypto.powers import KeyPairBasedPower, PowerUpError
|
||||||
from nucypher.crypto.signing import InvalidSignature
|
from nucypher.crypto.signing import InvalidSignature
|
||||||
from nucypher.datastore.datastore import Datastore
|
from nucypher.datastore.datastore import Datastore
|
||||||
from nucypher.datastore.models import ReencryptionRequest as ReencryptionRequestModel
|
from nucypher.datastore.models import ReencryptionRequest as ReencryptionRequestModel
|
||||||
from nucypher.policy.orders import ReencryptionRequest, ReencryptionResponse
|
|
||||||
from nucypher.network import LEARNING_LOOP_VERSION
|
from nucypher.network import LEARNING_LOOP_VERSION
|
||||||
from nucypher.network.exceptions import NodeSeemsToBeDown
|
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||||
from nucypher.network.protocols import InterfaceInfo
|
from nucypher.network.protocols import InterfaceInfo
|
||||||
|
from nucypher.network.retrieval import ReencryptionRequest, ReencryptionResponse
|
||||||
from nucypher.policy.hrac import HRAC
|
from nucypher.policy.hrac import HRAC
|
||||||
from nucypher.policy.kits import MessageKit
|
from nucypher.policy.kits import MessageKit
|
||||||
|
from nucypher.policy.revocation import Revocation
|
||||||
from nucypher.utilities.logging import Logger
|
from nucypher.utilities.logging import Logger
|
||||||
|
|
||||||
HERE = BASE_DIR = Path(__file__).parent
|
HERE = BASE_DIR = Path(__file__).parent
|
||||||
|
@ -271,7 +272,6 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) ->
|
||||||
|
|
||||||
@rest_app.route('/revoke', methods=['POST'])
|
@rest_app.route('/revoke', methods=['POST'])
|
||||||
def revoke():
|
def revoke():
|
||||||
from nucypher.policy.orders import Revocation
|
|
||||||
revocation = Revocation.from_bytes(request.data)
|
revocation = Revocation.from_bytes(request.data)
|
||||||
# TODO: Implement offchain revocation.
|
# TODO: Implement offchain revocation.
|
||||||
return Response(status=200)
|
return Response(status=200)
|
||||||
|
|
|
@ -16,12 +16,11 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from typing import Dict, NamedTuple, Optional, Tuple, List, Iterable
|
from typing import Dict, Optional, Iterable, Set
|
||||||
|
|
||||||
from bytestring_splitter import BytestringSplitter, BytestringKwargifier, VariableLengthBytestring
|
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
||||||
from constant_sorrow.constants import (
|
from constant_sorrow.constants import (
|
||||||
NOT_SIGNED,
|
NOT_SIGNED,
|
||||||
UNKNOWN_SENDER,
|
|
||||||
DO_NOT_SIGN,
|
DO_NOT_SIGN,
|
||||||
SIGNATURE_TO_FOLLOW,
|
SIGNATURE_TO_FOLLOW,
|
||||||
SIGNATURE_IS_ON_CIPHERTEXT,
|
SIGNATURE_IS_ON_CIPHERTEXT,
|
||||||
|
@ -30,7 +29,12 @@ from constant_sorrow.constants import (
|
||||||
from eth_typing import ChecksumAddress
|
from eth_typing import ChecksumAddress
|
||||||
from eth_utils import to_checksum_address, to_canonical_address
|
from eth_utils import to_checksum_address, to_canonical_address
|
||||||
|
|
||||||
from nucypher.crypto.splitters import capsule_splitter, key_splitter, signature_splitter, checksum_address_splitter
|
from nucypher.crypto.splitters import (
|
||||||
|
capsule_splitter,
|
||||||
|
key_splitter,
|
||||||
|
signature_splitter,
|
||||||
|
checksum_address_splitter,
|
||||||
|
)
|
||||||
import nucypher.crypto.umbral_adapter as umbral # need it to mock `umbral.encrypt`
|
import nucypher.crypto.umbral_adapter as umbral # need it to mock `umbral.encrypt`
|
||||||
from nucypher.crypto.umbral_adapter import PublicKey, VerifiedCapsuleFrag, Capsule, Signature
|
from nucypher.crypto.umbral_adapter import PublicKey, VerifiedCapsuleFrag, Capsule, Signature
|
||||||
|
|
||||||
|
@ -171,8 +175,6 @@ class PolicyMessageKit:
|
||||||
policy_key: PublicKey,
|
policy_key: PublicKey,
|
||||||
threshold: int
|
threshold: int
|
||||||
) -> 'PolicyMessageKit':
|
) -> 'PolicyMessageKit':
|
||||||
# TODO: can we get rid of circular dependency?
|
|
||||||
from nucypher.policy.orders import RetrievalResult
|
|
||||||
return cls(policy_key, threshold, RetrievalResult.empty(), message_kit)
|
return cls(policy_key, threshold, RetrievalResult.empty(), message_kit)
|
||||||
|
|
||||||
def as_retrieval_kit(self) -> RetrievalKit:
|
def as_retrieval_kit(self) -> RetrievalKit:
|
||||||
|
@ -216,39 +218,32 @@ class PolicyMessageKit:
|
||||||
message_kit=self.message_kit)
|
message_kit=self.message_kit)
|
||||||
|
|
||||||
|
|
||||||
class RevocationKit:
|
# TODO: a better name?
|
||||||
|
class RetrievalResult:
|
||||||
|
"""
|
||||||
|
An object representing retrieval results for a single capsule.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, treasure_map, signer: 'SignatureStamp'):
|
@classmethod
|
||||||
from nucypher.policy.orders import Revocation
|
def empty(cls):
|
||||||
self.revocations = dict()
|
return cls({})
|
||||||
for node_id, encrypted_kfrag in treasure_map:
|
|
||||||
self.revocations[node_id] = Revocation(ursula_checksum_address=node_id,
|
|
||||||
encrypted_kfrag=encrypted_kfrag,
|
|
||||||
signer=signer)
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __init__(self, cfrags: Dict[ChecksumAddress, VerifiedCapsuleFrag]):
|
||||||
return iter(self.revocations.values())
|
self.cfrags = cfrags
|
||||||
|
|
||||||
def __getitem__(self, node_id):
|
def addresses(self) -> Set[ChecksumAddress]:
|
||||||
return self.revocations[node_id]
|
return set(self.cfrags)
|
||||||
|
|
||||||
def __len__(self):
|
def with_result(self, result: 'RetrievalResult') -> 'RetrievalResult':
|
||||||
return len(self.revocations)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.revocations == other.revocations
|
|
||||||
|
|
||||||
@property
|
|
||||||
def revokable_addresses(self):
|
|
||||||
"""
|
"""
|
||||||
Returns a Set of revokable addresses in the checksum address formatting
|
Joins two RetrievalResult objects.
|
||||||
"""
|
|
||||||
return set(self.revocations.keys())
|
|
||||||
|
|
||||||
def add_confirmation(self, node_id, signed_receipt):
|
If both objects contain cfrags from the same Ursula,
|
||||||
|
the one from `result` will be kept.
|
||||||
"""
|
"""
|
||||||
Adds a signed confirmation of Ursula's ability to revoke the arrangement.
|
# TODO: would `+` or `|` operator be more suitable here?
|
||||||
"""
|
|
||||||
# TODO: Verify Ursula's signature
|
# TODO: check for overlap?
|
||||||
# TODO: Implement receipts
|
new_cfrags = dict(self.cfrags)
|
||||||
raise NotImplementedError
|
new_cfrags.update(result.cfrags)
|
||||||
|
return RetrievalResult(cfrags=new_cfrags)
|
||||||
|
|
|
@ -28,7 +28,6 @@ from nucypher.crypto.splitters import key_splitter
|
||||||
from nucypher.crypto.umbral_adapter import PublicKey, VerifiedKeyFrag, Signature
|
from nucypher.crypto.umbral_adapter import PublicKey, VerifiedKeyFrag, Signature
|
||||||
from nucypher.network.middleware import RestMiddleware
|
from nucypher.network.middleware import RestMiddleware
|
||||||
from nucypher.policy.hrac import HRAC
|
from nucypher.policy.hrac import HRAC
|
||||||
from nucypher.policy.kits import RevocationKit
|
|
||||||
from nucypher.policy.maps import TreasureMap
|
from nucypher.policy.maps import TreasureMap
|
||||||
from nucypher.policy.reservoir import (
|
from nucypher.policy.reservoir import (
|
||||||
make_federated_staker_reservoir,
|
make_federated_staker_reservoir,
|
||||||
|
@ -36,6 +35,7 @@ from nucypher.policy.reservoir import (
|
||||||
PrefetchStrategy,
|
PrefetchStrategy,
|
||||||
make_decentralized_staker_reservoir
|
make_decentralized_staker_reservoir
|
||||||
)
|
)
|
||||||
|
from nucypher.policy.revocation import RevocationKit
|
||||||
from nucypher.utilities.concurrency import WorkerPool
|
from nucypher.utilities.concurrency import WorkerPool
|
||||||
from nucypher.utilities.logging import Logger
|
from nucypher.utilities.logging import Logger
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
"""
|
||||||
|
This file is part of nucypher.
|
||||||
|
|
||||||
|
nucypher is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
nucypher is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from bytestring_splitter import BytestringSplitter
|
||||||
|
from eth_typing.evm import ChecksumAddress
|
||||||
|
from eth_utils.address import to_canonical_address, to_checksum_address
|
||||||
|
|
||||||
|
from nucypher.crypto.signing import SignatureStamp, InvalidSignature
|
||||||
|
from nucypher.crypto.splitters import signature_splitter, checksum_address_splitter
|
||||||
|
from nucypher.crypto.umbral_adapter import Signature, PublicKey
|
||||||
|
from nucypher.policy.kits import MessageKit
|
||||||
|
from nucypher.policy.maps import AuthorizedKeyFrag
|
||||||
|
|
||||||
|
|
||||||
|
class Revocation:
|
||||||
|
"""
|
||||||
|
Represents a string used by characters to perform a revocation on a specific
|
||||||
|
Ursula. It's a bytestring made of the following format:
|
||||||
|
REVOKE-<arrangement id to revoke><signature of the previous string>
|
||||||
|
This is sent as a payload in a DELETE method to the /KFrag/ endpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PREFIX = b'REVOKE-'
|
||||||
|
revocation_splitter = BytestringSplitter(
|
||||||
|
(bytes, len(PREFIX)),
|
||||||
|
checksum_address_splitter, # ursula canonical address
|
||||||
|
(bytes, AuthorizedKeyFrag.ENCRYPTED_SIZE), # encrypted kfrag payload (includes writ)
|
||||||
|
signature_splitter
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
ursula_checksum_address: ChecksumAddress, # TODO: Use staker address instead (what if the staker rebonds)?
|
||||||
|
encrypted_kfrag: MessageKit,
|
||||||
|
signer: Optional[SignatureStamp] = None,
|
||||||
|
signature: Optional[Signature] = None):
|
||||||
|
|
||||||
|
self.ursula_checksum_address = ursula_checksum_address
|
||||||
|
self.encrypted_kfrag = encrypted_kfrag
|
||||||
|
|
||||||
|
if not (bool(signer) ^ bool(signature)):
|
||||||
|
raise ValueError("Either pass a signer or a signature; not both.")
|
||||||
|
elif signer:
|
||||||
|
self.signature = signer(self.payload)
|
||||||
|
elif signature:
|
||||||
|
self.signature = signature
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return self.payload + bytes(self.signature)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return bytes(self)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(bytes(self))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return bytes(self) == bytes(other)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def payload(self):
|
||||||
|
return self.PREFIX \
|
||||||
|
+ to_canonical_address(self.ursula_checksum_address) \
|
||||||
|
+ bytes(self.encrypted_kfrag) \
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, revocation_bytes):
|
||||||
|
prefix, ursula_canonical_address, ekfrag, signature = cls.revocation_splitter(revocation_bytes)
|
||||||
|
ursula_checksum_address = to_checksum_address(ursula_canonical_address)
|
||||||
|
return cls(ursula_checksum_address=ursula_checksum_address,
|
||||||
|
encrypted_kfrag=ekfrag,
|
||||||
|
signature=signature)
|
||||||
|
|
||||||
|
def verify_signature(self, alice_verifying_key: PublicKey) -> bool:
|
||||||
|
"""
|
||||||
|
Verifies the revocation was from the provided pubkey.
|
||||||
|
"""
|
||||||
|
if not self.signature.verify(self.payload, alice_verifying_key):
|
||||||
|
raise InvalidSignature(f"Revocation has an invalid signature: {self.signature}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RevocationKit:
|
||||||
|
|
||||||
|
def __init__(self, treasure_map, signer: SignatureStamp):
|
||||||
|
self.revocations = dict()
|
||||||
|
for node_id, encrypted_kfrag in treasure_map:
|
||||||
|
self.revocations[node_id] = Revocation(ursula_checksum_address=node_id,
|
||||||
|
encrypted_kfrag=encrypted_kfrag,
|
||||||
|
signer=signer)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.revocations.values())
|
||||||
|
|
||||||
|
def __getitem__(self, node_id):
|
||||||
|
return self.revocations[node_id]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.revocations)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.revocations == other.revocations
|
||||||
|
|
||||||
|
@property
|
||||||
|
def revokable_addresses(self):
|
||||||
|
"""
|
||||||
|
Returns a Set of revokable addresses in the checksum address formatting
|
||||||
|
"""
|
||||||
|
return set(self.revocations.keys())
|
||||||
|
|
||||||
|
def add_confirmation(self, node_id, signed_receipt):
|
||||||
|
"""
|
||||||
|
Adds a signed confirmation of Ursula's ability to revoke the arrangement.
|
||||||
|
"""
|
||||||
|
# TODO: Verify Ursula's signature
|
||||||
|
# TODO: Implement receipts
|
||||||
|
raise NotImplementedError
|
|
@ -24,7 +24,7 @@ import pytest
|
||||||
from nucypher.characters.lawful import Enrico
|
from nucypher.characters.lawful import Enrico
|
||||||
from nucypher.crypto.utils import keccak_digest
|
from nucypher.crypto.utils import keccak_digest
|
||||||
from nucypher.policy.kits import MessageKit
|
from nucypher.policy.kits import MessageKit
|
||||||
from nucypher.policy.orders import Revocation
|
from nucypher.policy.revocation import Revocation
|
||||||
|
|
||||||
|
|
||||||
def test_federated_grant(federated_alice, federated_bob, federated_ursulas):
|
def test_federated_grant(federated_alice, federated_bob, federated_ursulas):
|
||||||
|
|
Loading…
Reference in New Issue