Extract kfrag authorization into a class

pull/2745/head
Bogdan Opanchuk 2021-07-09 22:45:25 -07:00
parent e5c0bb079e
commit 8c628da9ea
7 changed files with 114 additions and 65 deletions

View File

@ -108,7 +108,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
from nucypher.policy.maps import TreasureMap, AuthorizedKeyFrag
from nucypher.policy.orders import WorkOrder
from nucypher.policy.policies import Policy
from nucypher.utilities.logging import Logger
@ -1737,35 +1737,29 @@ class Ursula(Teacher, Character, Worker):
#
def verify_kfrag_authorization(self,
alice: Alice,
kfrag: KeyFrag,
signed_writ: bytes,
work_order: 'WorkOrder'
hrac: bytes,
author: Alice,
publisher: Alice,
authorized_kfrag: AuthorizedKeyFrag,
) -> VerifiedKeyFrag:
from nucypher.policy.orders import WorkOrder # TODO: resolve ciruclar dependency
writ_hrac, writ_kfrag_checksum, writ_signature = WorkOrder.signed_writ_splitter(signed_writ)
reconstructed_writ = writ_hrac + writ_kfrag_checksum
# TODO: should it be a method of AuthorizedKeyFrag?
try:
self.verify_from(alice, reconstructed_writ, signature=writ_signature)
self.verify_from(publisher, authorized_kfrag.writ, signature=authorized_kfrag.writ_signature)
except InvalidSignature:
# TODO (#2740): differentiate cases for Policy.Unauthorized
raise Policy.Unauthorized # This isn't from Alice (publisher).
if writ_hrac != work_order.hrac: # Funky Workorder
if authorized_kfrag.hrac != hrac: # Funky Workorder
raise Policy.Unauthorized # Bob, what the *hell* are you doing?
kfrag_checksum = keccak_digest(bytes(kfrag))[:WRIT_CHECKSUM_SIZE]
if kfrag_checksum != writ_kfrag_checksum:
raise Policy.Unauthorized # Bob, Seriously?
try:
verified_kfrag = kfrag.verify(verifying_pk=alice.stamp.as_umbral_pubkey())
verified_kfrag = authorized_kfrag.kfrag.verify(verifying_pk=author.stamp.as_umbral_pubkey())
except VerificationError:
raise Policy.Unauthorized # WTF, Alice did not generate these KFrags.
if writ_hrac in self.revoked_policies:
if authorized_kfrag.hrac in self.revoked_policies:
# Note: This is only an off-chain and in-memory check.
raise Policy.Unauthorized # Denied

View File

@ -24,7 +24,10 @@ SIGNATURE_SIZE = 64
EIP712_MESSAGE_SIGNATURE_SIZE = 65
WRIT_CHECKSUM_SIZE = 32
SIGNED_WRIT_SIZE = HRAC_LENGTH + WRIT_CHECKSUM_SIZE + SIGNATURE_SIZE
ENCRYPTED_KFRAG_PAYLOAD_LENGTH = 619 # Depends on encryption parameters in Umbral, has to be hardcoded
# The size of a serialized message kit encrypting an AuthorizedKeyFrag.
# Depends on encryption parameters in Umbral, has to be hardcoded.
ENCRYPTED_KFRAG_PAYLOAD_LENGTH = 619
# Digest Lengths
KECCAK_DIGEST_LENGTH = 32

View File

@ -220,14 +220,21 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) ->
return Response(response="KFrag decryption failed.", status=403) # 403 - Forbidden
# Verify KFrag Authorization (offchain)
signed_writ, kfrag = work_order.kfrag_payload_splitter(plaintext_kfrag_payload)
from nucypher.policy.maps import AuthorizedKeyFrag
try:
verified_kfrag = this_node.verify_kfrag_authorization(
alice=policy_publisher,
kfrag=kfrag,
signed_writ=signed_writ,
work_order=work_order
)
authorized_kfrag = AuthorizedKeyFrag.from_bytes(plaintext_kfrag_payload)
except ValueError:
message = f'{bob_identity_message} Invalid AuthorizedKeyFrag.'
log.info(message)
this_node.suspicious_activities_witnessed['unauthorized'].append(message)
return Response(message, status=401) # 401 - Unauthorized
try:
verified_kfrag = this_node.verify_kfrag_authorization(hrac=work_order.hrac,
author=alice,
publisher=policy_publisher,
authorized_kfrag=authorized_kfrag)
except Policy.Unauthorized:
message = f'{bob_identity_message} Unauthorized work order.'
log.info(message)

View File

@ -35,12 +35,68 @@ from nucypher.crypto.constants import HRAC_LENGTH, WRIT_CHECKSUM_SIZE, EIP712_ME
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
from nucypher.crypto.umbral_adapter import KeyFrag, PublicKey, Signature
from nucypher.crypto.splitters import signature_splitter, hrac_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 AuthorizedKeyFrag:
_splitter = BytestringSplitter(
hrac_splitter, # HRAC
BytestringSplitter((bytes, WRIT_CHECKSUM_SIZE)), # kfrag checksum
signature_splitter, # Publisher's signature
kfrag_splitter,
)
@staticmethod
def _kfrag_checksum(kfrag: KeyFrag) -> bytes:
return keccak_digest(bytes(kfrag))[:WRIT_CHECKSUM_SIZE]
@classmethod
def construct_by_publisher(cls,
hrac: bytes,
verified_kfrag: VerifiedKeyFrag,
publisher_stamp: SignatureStamp,
) -> 'AuthorizedKeyFrag':
# "un-verify" kfrag to keep further logic streamlined
kfrag = KeyFrag.from_bytes(bytes(verified_kfrag))
# Alice makes plain to Ursula that, upon decrypting this message,
# this particular KFrag is authorized for use in the policy identified by this HRAC.
kfrag_checksum = cls._kfrag_checksum(kfrag)
writ = hrac + kfrag_checksum
writ_signature = publisher_stamp(writ)
# The writ and the KFrag together represent a complete kfrag kit: the entirety of
# the material needed for Ursula to assuredly service this policy.
return cls(hrac, kfrag_checksum, writ_signature, kfrag)
def __init__(self, hrac: bytes, kfrag_checksum: bytes, writ_signature: Signature, kfrag: KeyFrag):
self.hrac = hrac
self.kfrag_checksum = kfrag_checksum
self.writ = hrac + kfrag_checksum
self.writ_signature = writ_signature
self.kfrag = kfrag
def __bytes__(self):
return self.writ + bytes(self.writ_signature) + bytes(self.kfrag)
@classmethod
def from_bytes(cls, data: bytes):
# TODO: should we check the signature right away here?
hrac, kfrag_checksum, writ_signature, kfrag = cls._splitter(data)
# Check integrity
calculated_checksum = cls._kfrag_checksum(kfrag)
if calculated_checksum != kfrag_checksum:
raise ValueError("Incorrect KeyFrag checksum in the serialized data")
return cls(hrac, kfrag_checksum, writ_signature, kfrag)
class TreasureMapSplitter(BrandingMixin, VersioningMixin, BytestringKwargifier):
pass
@ -214,24 +270,7 @@ class TreasureMap:
nodes_as_bytes += (node_id + kfrag)
return nodes_as_bytes
def _make_writ(self, kfrag, publisher_stamp) -> bytes:
"""
Alice makes plain to Ursula that, upon decrypting this message,
this particular KFrag is authorized for use in the policy identified by this HRAC.
"""
writ = self._hrac + keccak_digest(bytes(kfrag))[:WRIT_CHECKSUM_SIZE]
writ_signature = bytes(publisher_stamp(writ))
signed_writ = writ + writ_signature
return signed_writ
def _make_kfrag_payload(self, kfrag, publisher_stamp) -> bytes:
# The writ and the KFrag together represent a complete kfrag kit: the entirety of
# the material needed for Ursula to assuredly service this policy.
signed_writ = self._make_writ(kfrag=kfrag, publisher_stamp=publisher_stamp)
kfrag_payload = signed_writ + bytes(kfrag)
return kfrag_payload
def add_kfrag(self, ursula, kfrag, publisher_stamp: SignatureStamp) -> None:
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.")
@ -242,7 +281,9 @@ class TreasureMap:
'Cannot add KFrag to TreasureMap without an HRAC set. Call "derive_hrac" and try again.')
# Encrypt this kfrag payload for Ursula.
kfrag_payload = self._make_kfrag_payload(kfrag=kfrag, publisher_stamp=publisher_stamp)
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)
@ -295,7 +336,7 @@ class TreasureMap:
bob: 'Bob',
label: bytes,
ursulas: Sequence['Ursula'],
kfrags: Sequence[KeyFrag],
verified_kfrags: Sequence[VerifiedKeyFrag],
m: int
) -> 'TreasureMap':
"""Create a new treasure map for a collection of ursulas and kfrags."""
@ -307,9 +348,9 @@ class TreasureMap:
label=label)
# Encrypt each kfrag for an Ursula.
for ursula, kfrag in zip(ursulas, kfrags):
for ursula, verified_kfrag in zip(ursulas, verified_kfrags):
treasure_map.add_kfrag(ursula=ursula,
kfrag=kfrag,
verified_kfrag=verified_kfrag,
publisher_stamp=publisher.stamp)
# Sign the map if needed before sending it out into the world.

View File

@ -31,7 +31,7 @@ from nucypher.crypto.kits import RevocationKit
from nucypher.crypto.powers import TransactingPower
from nucypher.crypto.splitters import key_splitter
from nucypher.crypto.utils import keccak_digest
from nucypher.crypto.umbral_adapter import PublicKey, KeyFrag, Signature
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.reservoir import (
@ -196,7 +196,7 @@ class Policy(ABC):
label: bytes,
expiration: maya.MayaDT,
bob: 'Bob',
kfrags: Sequence[KeyFrag],
kfrags: Sequence[VerifiedKeyFrag],
public_key: PublicKey,
m: int,
):
@ -351,7 +351,7 @@ class Policy(ABC):
bob=self.bob,
label=self.label,
ursulas=list(arrangements),
kfrags=self.kfrags,
verified_kfrags=self.kfrags,
m=self.m)
return treasure_map
@ -418,7 +418,7 @@ class Policy(ABC):
raise NotImplementedError
@abstractmethod
def _make_enactment_payload(self, kfrag: KeyFrag) -> bytes:
def _make_enactment_payload(self, kfrag: VerifiedKeyFrag) -> bytes:
"""
Serializes a given kfrag and policy publication transaction to send to Ursula.
"""

View File

@ -26,6 +26,7 @@ from nucypher.crypto.kits import PolicyMessageKit
from nucypher.crypto.powers import DecryptingPower
from nucypher.crypto.umbral_adapter import reencrypt
from nucypher.datastore.models import Workorder
from nucypher.policy.maps import AuthorizedKeyFrag
from tests.utils.middleware import MockRestMiddleware, NodeIsDownMiddleware
@ -206,13 +207,16 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic
# Ursula decrypts the encrypted KFrag
encrypted_kfrag = enacted_federated_policy.treasure_map.destinations[ursula.checksum_address]
alice = Alice.from_public_keys(verifying_key=enacted_federated_policy.publisher_verifying_key)
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,
decrypt=True)
_signed_writ, the_kfrag = work_order.kfrag_payload_splitter(plaintext_kfrag_payload)
authorized_kfrag = AuthorizedKeyFrag.from_bytes(plaintext_kfrag_payload)
verified_kfrag = ursula.verify_kfrag_authorization(hrac=work_order.hrac,
author=alice,
publisher=alice,
authorized_kfrag=authorized_kfrag)
verified_kfrag = the_kfrag.verify(enacted_federated_policy.publisher_verifying_key)
reencrypt(capsule=message_kit.capsule, kfrag=verified_kfrag)
# Now we'll show that Ursula saved the correct WorkOrder.

View File

@ -22,18 +22,18 @@ import pytest
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.crypto.umbral_adapter import KeyFrag
from nucypher.policy.maps import TreasureMap
from nucypher.policy.maps import TreasureMap, AuthorizedKeyFrag
def test_complete_treasure_map_journey(federated_alice, federated_bob, federated_ursulas, mocker):
def test_complete_treasure_map_journey(federated_alice, federated_bob, federated_ursulas, idle_federated_policy, mocker):
treasure_map = TreasureMap(m=1)
bob_encrypting_key = federated_bob.public_keys(DecryptingPower)
bob_verifying_key = federated_bob.public_keys(SigningPower)
mock_kfrag = os.urandom(KeyFrag.serialized_size())
make_kfrag_payload_spy = mocker.spy(TreasureMap, '_make_kfrag_payload')
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,
@ -41,7 +41,7 @@ def test_complete_treasure_map_journey(federated_alice, federated_bob, federated
encrypted_kfrags = dict()
for ursula in federated_ursulas:
treasure_map.add_kfrag(ursula, mock_kfrag, federated_alice.stamp)
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,
@ -51,8 +51,8 @@ def test_complete_treasure_map_journey(federated_alice, federated_bob, federated
for ursula_address, encrypted_kfrag in treasure_map.destinations.items():
assert ursula_address in ursula_rolodex
ursula = ursula_rolodex[ursula_address]
mock_kfrag_payload = encrypted_kfrags[ursula.checksum_address]
assert mock_kfrag_payload == ursula.verify_from(federated_alice, encrypted_kfrag, decrypt=True) # FIXME: 2203
kfrag_payload = encrypted_kfrags[ursula.checksum_address]
assert kfrag_payload == ursula.verify_from(federated_alice, encrypted_kfrag, decrypt=True) # FIXME: 2203
serialized_map = bytes(treasure_map)
# ...
@ -67,16 +67,16 @@ def test_complete_treasure_map_journey(federated_alice, federated_bob, federated
@pytest.mark.skip(reason='Backwards-incompatible with umbral 0.2+')
def test_treasure_map_versioning(mocker, federated_alice, federated_bob, federated_ursulas):
def test_treasure_map_versioning(mocker, federated_alice, federated_bob, federated_ursulas, idle_federated_policy):
# Produced using f04d564a1
map_from_previous_version = b'\x87T\x19\xceV_1\x8e\xb0\x87\xf6\xd9\x9d\x80\xba\xaf\xc4\x84\xa1\xd9|P=\x02\x13\xa0r1\x9eB\xf4\xfc\xc6w\xdf\xd1\x88\xc4\x83\x8f \x1c|\xec\xfcnW~k\x95f8\x19\r\xb1\xad\xe9\xa8\xc9\x06\x93j\xaf\xc50&[\xe5Cy\x9cr_R\xcd\xb1\xb1F\xed\x01\x00\x00\x02\xf4\x02][\xb8VP\xfa%D\xc3\xeb\xd4\x8b\xd2SW\x0f\xfe\xe5\x0f\xaa\xe6\x83\x9a\xa1\x91\xf6\x8e\xca\x00\x95\xf9\x90\x02-\x7f\xca\xe8$L\xcd0\x1d\xa1D\x80\xafjY\xea2\xbc\x04\x94\x1c\xd6E\xa4l\x8fu\xdf\x8a#\x04\xe1\x8eKN\xc9Y\xfbB7I\x9b\xa153\xcef\xfd\xb2/9[\x1b^\xe3\xcf\x08/\xf4%k\x06\xf4\xa5\x03\xfa\xf1\xdc\xec\xe1\t\xeb%\x0c\x11{\xbb\xc7Z\xb2^\x1d.\x18\xeaJ\xaa\xa6f\xd8\xb0\x92U\x84;\xbe6\x00\x00\x02m\x89\x97?\xcavL\xa7q\x13\x01\x1e\x1f6\x05)\xc2?\xcd\x96\xafhH/>6\x8d\x1a\xf8\xfd\xd5\x8a\xf9e\xb0\xc5\xa8\xbd(\x86\x9f\xb9L\xb9n=\xcb\xa0\xd2\t\x94\x90l\xc0\xb7\x85\x90N\xe0\xc9M{\x08\xc4\xf5\x80\xb7\xd1\x10\x18P\x8bl\x0f\x87fS\x836\xa6q\'\xabr\xd1l\x1e\xe2\xe7\xce\xccZ1[\x0b\xe7\xaa\x9c\x92Qh"2F\x1f\x9f-7HylC\xad\x03\x8ek_\xb6M\x19\xb2\xef\xde~\xa6\x10F<\xac\x94\xa6e\xc3\xb5\x132\x94\x96\xc4\xd9\'\xf9h\x1c\xe8\xb8Zm\x86M\xed\x00\x86\xc3\xf4\x93\x03/J\x1d6$\x1a\xe5+\xad\xf93\x17n\xc3\x19sQ2C\xaf\x9d\x89p\xb9557O\x9a\xc3O\xf0\x1f\xb3M.\xa9\x89\xeb\xb9\xf6\xe8\xcc@\xb0\\)\x9d\xdb\'\xfc\xc4_\xfd\xe1\xef\x01\xe3\xe7va\xac\xd7y\xb2\xcfm\xda\x85\x06(\x92H\xe2p\xf1\x9aw\xaf\x83\x1c\xd3@a\xaa\xf6\xee\xfc\xae&;\xdd*\x94I\'r1JG\xca\xdb\x9e\xef\x18Z\x9f\x15\x81\xe3\x1c\xcfJ\xd6;2H\xe8\xed\xfc\x98\x8e\xc6\x94\x1f\x1d\x95A\xa5\x8e\xe5\xc6f\x85\xbb\xc3\xd0\x9d\x83\xd3\xdf\x91]\x16\xe6)\xfa\xc0\xf3\xba\x7fAb\x81\xe0\x8f\x1bu0\x0b\x82^\xe9\x16\xf0\xfc\xc3p\xd4\x9f\'\xa6\xe5\xb4\xf7\xe1\x99\xa5\xfe\x12\x0e{L\xb0\xd6\xa1\x049\xcf\xe0\xca\x06\xe3\xd6u\x9e\xb3P\xb7\x1a\xc5X\xb7\xb2\xfa\x1dJ\xe1\xa9Gb\xf6l~DG\x8e5X\xc2^\x87\xac\x89W(\xaf\xd3\x15o\xde\xf7\xe4\x18\xd9\x98\xc3\tcL\xd3\x9dF\x8e3\xe5u\x03\x0b\xe7\tj\xdb\xd3B\xa1\x85\x9d \x9c\xa4{n\x01"\xab\xe1509\xdaoL\xc9\x8d\xc9\xfd"\xad\xd8\xfd\xf5\x14\xa2\xa8N\xf5\xa0\xf4\x04Y\x85i\xe0zj34\xc9\xbd\xac\xb9gn\x19J]\x0eL\x81C\xb9\x95\x86Q,\x81\xdf\xcbh\x13\xae8\xe8\x06y\xd1\xcd\x867\x1a\x1c\xe1\x05\xba\xfaL\x1a\x1f\x9f~\x18O1p@\xee\xee\xc4\xed\x84%\xb4\xb4\x12\xb6\x81\x0c\xcamf.\x9c\xe1\xfe\xc4\x87I\'\xc7e\xc1\x7f\xeb\x9c\xe1\xca\xa5\r.\x15\xa8r\xa8\x82Q\x13\x99K\x12X3\x04\xbc\x99\x96\xf8\xc3\x1es\x0c\x85\x8d\xd3\xee\x1b^\xc8\xf5\x1d^\x1a&6#\xbc\xa8~wp}]8\xb5\xe6v\xa4D\xfe:\xb8<q\xd9\x02\xfa\x7f\xcfWA\xad\xd1#\xac\x8b\xd7\xff\xca\xf7[dm\x9b\x06\xcc\x03\x1b\xfa\xd1\xf6:\xad\x1c\xb6\xb8'
kfrags = [os.urandom(32) for _ in range(3)]
kfrags = idle_federated_policy.kfrags[:3]
treasure_map = TreasureMap.construct_by_publisher(publisher=federated_alice,
bob=federated_bob,
label=b'still Bill',
ursulas=list(federated_ursulas)[:len(kfrags)],
kfrags=kfrags,
verified_kfrags=kfrags,
m=2)
# Good version (baseline)