Merge pull request #278 from jMyles/umbral-compat

Uses Alice's Stamp as a signer for KFrags; Bob and Ursula validate them as such.
pull/276/head^2
Justin Holmes 2018-05-29 18:35:54 -04:00 committed by GitHub
commit 19b0f71555
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 137 additions and 174 deletions

View File

@ -21,7 +21,7 @@ apistar = "*"
mypy = "*"
pytest-mypy = "*"
maya = "*"
pyumbral = {git = "https://github.com/nucypher/pyumbral.git"}
pyumbral = {git = "https://github.com/nucypher/pyumbral.git", ref = "kfrag-signing"}
requests = "*"
hendrix = {git = "https://github.com/hendrix/hendrix", ref = "tags/3.0.0rc1"}
constantSorrow = {git = "https://github.com/nucypher/constantSorrow.git", ref = "nucypher-depend"}

View File

@ -19,7 +19,7 @@ from nucypher.crypto.api import secure_random, keccak_digest, encrypt_and_sign
from nucypher.crypto.constants import PUBLIC_KEY_LENGTH
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import CryptoPower, SigningPower, EncryptingPower, DelegatingPower, NoSigningPower
from nucypher.crypto.signature import Signature, signature_splitter, SignatureStamp, StrangerStamp
from nucypher.crypto.signing import Signature, signature_splitter, SignatureStamp, StrangerStamp
from nucypher.network import blockchain_client
from nucypher.network.protocols import dht_value_splitter, dht_with_hrac_splitter
from nucypher.network.server import NucypherDHTServer, NucypherSeedOnlyDHTServer, ProxyRESTServer
@ -79,7 +79,8 @@ class Character(object):
if is_me:
self.network_middleware = network_middleware or NetworkyStuff()
try:
self._stamp = SignatureStamp(self._crypto_power.power_ups(SigningPower).keypair)
signing_power = self._crypto_power.power_ups(SigningPower)
self._stamp = signing_power.get_signature_stamp()
except NoSigningPower:
self._stamp = constants.NO_SIGNING_POWER
@ -87,8 +88,9 @@ class Character(object):
self.attach_server()
else:
if network_middleware is not None:
raise TypeError("Can't attach network middleware to a Character who isn't me. What are you even trying to do?")
self._stamp = StrangerStamp(self._crypto_power.power_ups(SigningPower).keypair)
raise TypeError(
"Can't attach network middleware to a Character who isn't me. What are you even trying to do?")
self._stamp = StrangerStamp(self.public_key(SigningPower))
def __eq__(self, other):
return bytes(self.stamp) == bytes(other.stamp)
@ -189,6 +191,7 @@ class Character(object):
message_kit: Union[UmbralMessageKit, bytes],
signature: Signature = None,
decrypt=False,
delegator_signing_key: UmbralPublicKey = None,
) -> tuple:
"""
Inverse of encrypt_for.
@ -196,11 +199,14 @@ class Character(object):
:param actor_that_sender_claims_to_be: A Character instance representing
the actor whom the sender claims to be. We check the public key
owned by this Character instance to verify.
:param messages: The messages to be verified.
:param message_kit: the message to be (perhaps decrypted and) verified.
:param signature: The signature to check.
:param decrypt: Whether or not to decrypt the messages.
:param signature_is_on_cleartext: True if we expect the signature to be
on the cleartext. Otherwise, we presume that the ciphertext is what
is signed.
:param delegator_signing_key: A signing key from the original delegator.
This is used only when decrypting a MessageKit with an activated Capsule
to check that the KFrag used to create each attached CFrag is the
authentic KFrag initially created by the delegator.
:return: Whether or not the signature is valid, the decrypted plaintext
or NO_DECRYPTION_PERFORMED
"""
@ -215,7 +221,7 @@ class Character(object):
if decrypt:
# We are decrypting the message; let's do that first and see what the sig header says.
cleartext_with_sig_header = self.decrypt(message_kit)
cleartext_with_sig_header = self.decrypt(message_kit, verifying_key=delegator_signing_key)
sig_header, cleartext = default_constant_splitter(cleartext_with_sig_header, return_remainder=True)
if sig_header == constants.SIGNATURE_IS_ON_CIPHERTEXT:
# THe ciphertext is what is signed - note that for later.
@ -254,8 +260,8 @@ class Character(object):
If they don't have the correct Power, the appropriate PowerUpError is raised.
"""
def decrypt(self, message_kit):
return self._crypto_power.power_ups(EncryptingPower).decrypt(message_kit)
def decrypt(self, message_kit, verifying_key: UmbralPublicKey = None):
return self._crypto_power.power_ups(EncryptingPower).decrypt(message_kit, verifying_key)
def sign(self, message):
return self._crypto_power.power_ups(SigningPower).sign(message)
@ -302,7 +308,8 @@ class Character(object):
powers_and_keys=({SigningPower: pubkey})
)
else:
message = "Suspicious Activity: Discovered node with bad signature: {}. Propagated by: {}:{}".format(node_meta, address, port)
message = "Suspicious Activity: Discovered node with bad signature: {}. Propagated by: {}:{}".format(
node_meta, address, port)
self.log.warn(message)
return new_nodes
@ -335,7 +342,8 @@ class Alice(Character, PolicyAuthor):
:param n: Total number of kfrags to generate
"""
bob_pubkey_enc = bob.public_key(EncryptingPower)
return self._crypto_power.power_ups(DelegatingPower).generate_kfrags(bob_pubkey_enc, label, m, n)
delegating_power = self._crypto_power.power_ups(DelegatingPower)
return delegating_power.generate_kfrags(bob_pubkey_enc, self.stamp, label, m, n)
def create_policy(self, bob: "Bob", label: bytes, m: int, n: int):
"""

View File

@ -117,7 +117,7 @@ class DerivedKeyBasedPower(CryptoPowerUp):
class SigningPower(KeyPairBasedPower):
_keypair_class = SigningKeypair
not_found_error = NoSigningPower
provides = ("sign", "generate_self_signed_cert")
provides = ("sign", "generate_self_signed_cert", "get_signature_stamp")
class EncryptingPower(KeyPairBasedPower):
@ -131,7 +131,7 @@ class DelegatingPower(DerivedKeyBasedPower):
def __init__(self):
self.umbral_keying_material = UmbralKeyingMaterial()
def generate_kfrags(self, bob_pubkey_enc, label, m, n) -> Union[UmbralPublicKey, List]:
def generate_kfrags(self, bob_pubkey_enc, signer, label, m, n) -> Union[UmbralPublicKey, List]:
"""
Generates re-encryption key frags ("KFrags") and returns them.
@ -144,5 +144,5 @@ class DelegatingPower(DerivedKeyBasedPower):
# TODO: salt? #265
__private_key = self.umbral_keying_material.derive_privkey_by_label(label)
kfrags = pre.split_rekey(__private_key, bob_pubkey_enc, m, n)
kfrags = pre.split_rekey(__private_key, signer, bob_pubkey_enc, m, n)
return __private_key.get_pubkey(), kfrags

View File

@ -1,123 +0,0 @@
from nucypher.crypto import api as API
from umbral.keys import UmbralPublicKey
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
from nucypher.crypto.api import keccak_digest
from bytestring_splitter import BytestringSplitter
class Signature(object):
"""
The Signature object allows signatures to be made and verified.
"""
_EXPECTED_LENGTH = 64 # With secp256k1
def __init__(self, r: int, s: int):
# TODO: Sanity check for proper r and s.
self.r = r
self.s = s
def __repr__(self):
return "ECDSA Signature: {}".format(bytes(self).hex()[:15])
def verify(self, message: bytes, pubkey: UmbralPublicKey) -> bool:
"""
Verifies that a message's signature was valid.
:param message: The message to verify
:param pubkey: UmbralPublicKey of the signer
:return: True if valid, False if invalid
"""
return API.ecdsa_verify(message, self._der_encoded_bytes(), pubkey)
@classmethod
def from_bytes(cls, signature_as_bytes, der_encoded=False):
# TODO: Change the int literals to variables which account for the order of the curve.
if der_encoded:
r, s = decode_dss_signature(signature_as_bytes)
else:
if not len(signature_as_bytes) == 64:
raise ValueError("Looking for exactly 64 bytes if you call from_bytes with der_encoded=False.")
else:
r = int.from_bytes(signature_as_bytes[:32], "big")
s = int.from_bytes(signature_as_bytes[32:], "big")
return cls(r, s)
def _der_encoded_bytes(self):
return encode_dss_signature(self.r, self.s)
def __bytes__(self):
return self.r.to_bytes(32, "big") + self.s.to_bytes(32, "big")
def __len__(self):
return len(bytes(self))
def __add__(self, other):
return bytes(self) + other
def __radd__(self, other):
return other + bytes(self)
def __eq__(self, other):
# TODO: Consider constant time
return bytes(self) == bytes(other) or self._der_encoded_bytes() == other
signature_splitter = BytestringSplitter(Signature)
class SignatureStamp(object):
"""
Can be called to sign something or used to express the signing public
key as bytes.
"""
def __init__(self, signing_keypair):
self._sign = signing_keypair.sign
self._as_bytes = bytes(signing_keypair.pubkey)
self._as_umbral_pubkey = signing_keypair.pubkey
def __bytes__(self):
return self._as_bytes
def __call__(self, *args, **kwargs):
return self._sign(*args, **kwargs)
def __hash__(self):
return int.from_bytes(self, byteorder="big")
def __eq__(self, other):
return other == bytes(self)
def __add__(self, other):
return bytes(self) + other
def __radd__(self, other):
return other + bytes(self)
def __len__(self):
return len(bytes(self))
def __bool__(self):
return True
def as_umbral_pubkey(self):
return self._as_umbral_pubkey
def fingerprint(self):
"""
Hashes the key using keccak-256 and returns the hexdigest in bytes.
:return: Hexdigest fingerprint of key (keccak-256) in bytes
"""
return keccak_digest(bytes(self)).hex().encode()
class StrangerStamp(SignatureStamp):
"""
SignatureStamp of a stranger (ie, can only be used to glean public key, not to sign)
"""
def __call__(self, *args, **kwargs):
message = "This isn't your SignatureStamp; it belongs to (a Stranger). You can't sign with it."
raise TypeError(message)

View File

@ -0,0 +1,61 @@
from nucypher.crypto.api import keccak_digest
from bytestring_splitter import BytestringSplitter
from umbral.signing import Signature, Signer
signature_splitter = BytestringSplitter(Signature)
class SignatureStamp(object):
"""
Can be called to sign something or used to express the signing public
key as bytes.
"""
def __init__(self, signing_key, signer: Signer=None):
self.__signer = signer
self._as_bytes = bytes(signing_key)
self._as_umbral_pubkey = signing_key
def __bytes__(self):
return self._as_bytes
def __call__(self, *args, **kwargs):
return self.__signer(*args, **kwargs)
def __hash__(self):
return int.from_bytes(self, byteorder="big")
def __eq__(self, other):
return other == bytes(self)
def __add__(self, other):
return bytes(self) + other
def __radd__(self, other):
return other + bytes(self)
def __len__(self):
return len(bytes(self))
def __bool__(self):
return True
def as_umbral_pubkey(self):
return self._as_umbral_pubkey
def fingerprint(self):
"""
Hashes the key using keccak-256 and returns the hexdigest in bytes.
:return: Hexdigest fingerprint of key (keccak-256) in bytes
"""
return keccak_digest(bytes(self)).hex().encode()
class StrangerStamp(SignatureStamp):
"""
SignatureStamp of a stranger (ie, can only be used to glean public key, not to sign)
"""
def __call__(self, *args, **kwargs):
message = "This isn't your SignatureStamp; it belongs to (a Stranger). You can't sign with it."
raise TypeError(message)

View File

@ -1,17 +1,20 @@
from nucypher.crypto.api import encrypt_and_sign
from nucypher.crypto.signature import SignatureStamp
from nucypher.crypto.powers import SigningPower
from nucypher.crypto.signing import SignatureStamp
from nucypher.keystore.keypairs import SigningKeypair
from constant_sorrow.constants import NO_SIGNING_POWER
from umbral.keys import UmbralPublicKey
from umbral.signing import Signer
class DataSource:
def __init__(self, policy_pubkey_enc, signer=NO_SIGNING_POWER, label=None):
def __init__(self, policy_pubkey_enc, signing_keypair=NO_SIGNING_POWER, label=None):
self.policy_pubkey = policy_pubkey_enc
if signer is NO_SIGNING_POWER:
signer = SignatureStamp(SigningKeypair()) # TODO: Generate signing key properly. #241
self.stamp = signer
if signing_keypair is NO_SIGNING_POWER:
signing_keypair = SigningKeypair() # TODO: Generate signing key properly. #241
signing_power = SigningPower(keypair=signing_keypair)
self.stamp = signing_power.get_signature_stamp()
self.label = label
def encapsulate_single_message(self, message):

View File

@ -7,13 +7,15 @@ from umbral.keys import UmbralPrivateKey, UmbralPublicKey
from umbral import pre
from umbral.config import default_curve
from nucypher.crypto.kits import MessageKit
from nucypher.crypto.signature import Signature
from nucypher.crypto.signing import SignatureStamp
from umbral.signing import Signature, Signer
class Keypair(object):
"""
A parent Keypair class for all types of Keypairs.
"""
def __init__(self,
umbral_key: Union[UmbralPrivateKey, UmbralPublicKey] = None,
generate_keys_if_needed=True):
@ -34,7 +36,8 @@ class Keypair(object):
self._privkey = UmbralPrivateKey.gen_key()
self.pubkey = self._privkey.get_pubkey()
else:
raise ValueError("Either pass a valid key as umbral_key or, if you want to generate keys, set generate_keys_if_needed to True.")
raise ValueError(
"Either pass a valid key as umbral_key or, if you want to generate keys, set generate_keys_if_needed to True.")
def serialize_pubkey(self, as_b64=False) -> bytes:
"""
@ -61,10 +64,11 @@ class EncryptingKeypair(Keypair):
"""
A keypair for Umbral
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def decrypt(self, message_kit: MessageKit) -> bytes:
def decrypt(self, message_kit: MessageKit, verifying_key: UmbralPublicKey = None) -> bytes:
"""
Decrypt data encrypted with Umbral.
@ -72,8 +76,10 @@ class EncryptingKeypair(Keypair):
"""
cleartext = pre.decrypt(ciphertext=message_kit.ciphertext,
capsule=message_kit.capsule,
priv_key=self._privkey,
alice_pub_key=message_kit.policy_pubkey)
decrypting_key=self._privkey,
delegating_pubkey=message_kit.policy_pubkey,
verifying_key=verifying_key,
)
return cleartext
@ -82,6 +88,7 @@ class SigningKeypair(Keypair):
"""
A SigningKeypair that uses ECDSA.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -99,3 +106,7 @@ class SigningKeypair(Keypair):
def generate_self_signed_cert(self, common_name):
cryptography_key = self._privkey.to_cryptography_privkey()
return generate_self_signed_certificate(common_name, default_curve(), cryptography_key)
def get_signature_stamp(self):
signer = Signer(self._privkey)
return SignatureStamp(signing_key=self.pubkey, signer=signer)

View File

@ -1,6 +1,6 @@
from typing import Union
from nucypher.crypto.signature import Signature
from nucypher.crypto.signing import Signature
from bytestring_splitter import BytestringSplitter
from nucypher.keystore.db.models import Key, PolicyArrangement, Workorder
from umbral.fragments import KFrag
@ -21,7 +21,7 @@ class KeyStore(object):
"""
A storage class of cryptographic keys.
"""
kfrag_splitter = BytestringSplitter(Signature, (KFrag, KFrag.get_size()))
kfrag_splitter = BytestringSplitter(Signature, (KFrag, KFrag.expected_bytes_length()))
def __init__(self, sqlalchemy_engine=None):
"""

View File

@ -5,7 +5,7 @@ from kademlia.utils import digest
from constant_sorrow import default_constant_splitter, constants
from nucypher.crypto.api import keccak_digest
from nucypher.crypto.constants import PUBLIC_KEY_LENGTH, KECCAK_DIGEST_LENGTH
from nucypher.crypto.signature import Signature
from nucypher.crypto.signing import Signature
from bytestring_splitter import BytestringSplitter
from nucypher.network.node import NucypherNode
from nucypher.network.routing import NucypherRoutingTable

View File

@ -12,7 +12,7 @@ from nucypher.characters import Bob, Ursula
from nucypher.crypto.api import keccak_digest
from nucypher.crypto.constants import KECCAK_DIGEST_LENGTH
from nucypher.crypto.powers import SigningPower, DelegatingPower
from nucypher.crypto.signature import Signature
from nucypher.crypto.signing import Signature
from nucypher.crypto.splitters import key_splitter
from bytestring_splitter import BytestringSplitter
from nucypher.blockchain.eth.policies import BlockchainArrangement

View File

@ -7,7 +7,6 @@ from nucypher.crypto.constants import CFRAG_LENGTH_WITHOUT_PROOF
def test_bob_cannot_follow_the_treasure_map_in_isolation(enacted_policy, bob):
# Assume for the moment that Bob has already received a TreasureMap, perhaps via a side channel.
hrac, treasure_map = enacted_policy.hrac(), enacted_policy.treasure_map
bob.treasure_maps[hrac] = treasure_map
@ -134,11 +133,13 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_policy, bob,
the_correct_cfrag = pre.reencrypt(the_kfrag, capsule)
# The first CFRAG_LENGTH_WITHOUT_PROOF bytes (ie, the cfrag proper, not the proof material), are the same:
assert bytes(the_cfrag)[:CapsuleFrag.get_size()] == bytes(the_correct_cfrag)[:CapsuleFrag.get_size()] # It's the correct cfrag!
assert bytes(the_cfrag)[:CapsuleFrag.expected_bytes_length()] == bytes(the_correct_cfrag)[
:CapsuleFrag.expected_bytes_length()] # It's the correct cfrag!
assert the_correct_cfrag.verify_correctness(capsule,
pubkey_a=enacted_policy.public_key,
pubkey_b=bob.public_key(EncryptingPower))
delegating_pubkey=enacted_policy.public_key,
signing_pubkey=alice.stamp.as_umbral_pubkey(),
encrypting_pubkey=bob.public_key(EncryptingPower))
# Now we'll show that Ursula saved the correct WorkOrder.
work_orders_from_bob = ursula.work_orders(bob=bob)
@ -187,7 +188,7 @@ def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_polic
capsule_side_channel[0].capsule.attach_cfrag(new_cfrag)
def test_bob_gathers_and_combines(enacted_policy, bob, ursulas, capsule_side_channel):
def test_bob_gathers_and_combines(enacted_policy, bob, alice, capsule_side_channel):
# The side channel is represented as a single MessageKit, which is all that Bob really needs.
the_message_kit, the_data_source = capsule_side_channel
@ -199,7 +200,7 @@ def test_bob_gathers_and_combines(enacted_policy, bob, ursulas, capsule_side_cha
# Bob can't decrypt yet with just two CFrags. He needs to gather at least m.
with pytest.raises(pre.GenericUmbralError):
bob.decrypt(the_message_kit)
bob.decrypt(the_message_kit, verifying_key=alice.stamp.as_umbral_pubkey())
number_left_to_collect = enacted_policy.treasure_map.m - len(bob._saved_work_orders)
@ -213,6 +214,8 @@ def test_bob_gathers_and_combines(enacted_policy, bob, ursulas, capsule_side_cha
# Now.
# At long last.
is_valid, cleartext = bob.verify_from(the_data_source, the_message_kit, decrypt=True)
is_valid, cleartext = bob.verify_from(the_data_source, the_message_kit,
decrypt=True,
delegator_signing_key=alice.stamp.as_umbral_pubkey())
assert cleartext == b'Welcome to the flippering.'
assert is_valid

View File

@ -1,6 +1,6 @@
import pytest
from nucypher.crypto.api import secure_random
from nucypher.crypto.signature import Signature
from nucypher.crypto.signing import Signature
from bytestring_splitter import BytestringSplitter

View File

@ -1,6 +1,6 @@
from nucypher.crypto.api import ecdsa_sign
from umbral.keys import UmbralPrivateKey
from nucypher.crypto.signature import Signature
from nucypher.crypto.signing import Signature
def test_signature_can_verify():

View File

@ -11,7 +11,7 @@ from nucypher.characters import Alice, Bob
from nucypher.keystore import keystore
from nucypher.keystore.db import Base
from nucypher.crypto.signature import SignatureStamp
from nucypher.crypto.signing import SignatureStamp
from nucypher.data_sources import DataSource
from nucypher.keystore import keystore
from nucypher.keystore.db import Base
@ -97,7 +97,7 @@ def test_keystore():
def capsule_side_channel(enacted_policy):
signing_keypair = SigningKeypair()
data_source = DataSource(policy_pubkey_enc=enacted_policy.public_key,
signer=SignatureStamp(signing_keypair))
signing_keypair=signing_keypair)
message_kit, _signature = data_source.encapsulate_single_message(b"Welcome to the flippering.")
return message_kit, data_source