Move retrieval machinery to network/retrieval and revocation to policy/revocation

pull/2730/head
Bogdan Opanchuk 2021-09-02 22:22:24 -07:00
parent 1ad868bf41
commit a9cc13e825
7 changed files with 310 additions and 292 deletions

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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):