mirror of https://github.com/nucypher/nucypher.git
Merge pull request #1048 from KPrasch/verify
Decentralized Identity Verification Fixes and Namingpull/951/head
commit
6f1fa31aa8
|
@ -717,7 +717,7 @@ class Ursula(Teacher, Character, Miner):
|
|||
timestamp=None,
|
||||
|
||||
# Blockchain
|
||||
identity_evidence: bytes = constants.NOT_SIGNED,
|
||||
decentralized_identity_evidence: bytes = constants.NOT_SIGNED,
|
||||
checksum_public_address: str = None,
|
||||
|
||||
# Character
|
||||
|
@ -765,12 +765,12 @@ class Ursula(Teacher, Character, Miner):
|
|||
if not federated_only:
|
||||
Miner.__init__(self, is_me=is_me, checksum_address=checksum_public_address)
|
||||
|
||||
# Access staking node via node's transacting keys TODO: Better handle ephemeral staking self ursula
|
||||
# Access staking node via node's transacting keys TODO: Better handling of ephemeral staking self ursula?
|
||||
blockchain_power = BlockchainPower(blockchain=self.blockchain, account=self.checksum_public_address)
|
||||
self._crypto_power.consume_power_up(blockchain_power)
|
||||
|
||||
# Use blockchain power to substantiate stamp, instead of signing key
|
||||
self.substantiate_stamp(password=password) # TODO: Derive from keyring
|
||||
self.substantiate_stamp(client_password=password) # TODO: Derive from keyring
|
||||
|
||||
#
|
||||
# ProxyRESTServer and TLSHostingPower # TODO: Maybe we want _power_ups to be public after all?
|
||||
|
@ -841,7 +841,7 @@ class Ursula(Teacher, Character, Miner):
|
|||
certificate_filepath=certificate_filepath,
|
||||
interface_signature=interface_signature,
|
||||
timestamp=timestamp,
|
||||
identity_evidence=identity_evidence,
|
||||
decentralized_identity_evidence=decentralized_identity_evidence,
|
||||
substantiate_immediately=is_me and not federated_only,
|
||||
# FIXME: When is_me and not federated_only, the stamp is substantiated twice
|
||||
# See line 728 above.
|
||||
|
@ -880,7 +880,7 @@ class Ursula(Teacher, Character, Miner):
|
|||
|
||||
version = self.TEACHER_VERSION.to_bytes(2, "big")
|
||||
interface_info = VariableLengthBytestring(bytes(self.rest_information()[0]))
|
||||
identity_evidence = VariableLengthBytestring(self._identity_evidence)
|
||||
decentralized_identity_evidence = VariableLengthBytestring(self.decentralized_identity_evidence)
|
||||
|
||||
certificate = self.rest_server_certificate()
|
||||
cert_vbytes = VariableLengthBytestring(certificate.public_bytes(Encoding.PEM))
|
||||
|
@ -891,7 +891,7 @@ class Ursula(Teacher, Character, Miner):
|
|||
bytes(VariableLengthBytestring.bundle(domains)),
|
||||
self.timestamp_bytes(),
|
||||
bytes(self._interface_signature),
|
||||
bytes(identity_evidence),
|
||||
bytes(decentralized_identity_evidence),
|
||||
bytes(self.public_keys(SigningPower)),
|
||||
bytes(self.public_keys(DecryptingPower)),
|
||||
bytes(cert_vbytes),
|
||||
|
@ -1041,7 +1041,7 @@ class Ursula(Teacher, Character, Miner):
|
|||
domains=VariableLengthBytestring,
|
||||
timestamp=(int, 4, {'byteorder': 'big'}),
|
||||
interface_signature=Signature,
|
||||
identity_evidence=VariableLengthBytestring,
|
||||
decentralized_identity_evidence=VariableLengthBytestring,
|
||||
verifying_key=(UmbralPublicKey, PUBLIC_KEY_LENGTH),
|
||||
encrypting_key=(UmbralPublicKey, PUBLIC_KEY_LENGTH),
|
||||
certificate=(load_pem_x509_certificate, VariableLengthBytestring, {"backend": default_backend()}),
|
||||
|
|
|
@ -58,10 +58,10 @@ class Vladimir(Ursula):
|
|||
rest_port=target_ursula.rest_information()[0].port,
|
||||
certificate=target_ursula.rest_server_certificate(),
|
||||
network_middleware=cls.network_middleware,
|
||||
checksum_public_address = cls.fraud_address,
|
||||
checksum_public_address=cls.fraud_address,
|
||||
######### Asshole.
|
||||
timestamp=target_ursula._timestamp,
|
||||
interface_signature=target_ursula._interface_signature_object,
|
||||
interface_signature=target_ursula._interface_signature,
|
||||
#########
|
||||
)
|
||||
|
||||
|
|
|
@ -84,29 +84,29 @@ def keccak_digest(*messages: bytes) -> bytes:
|
|||
:rtype: bytes
|
||||
:return: bytestring of digested data
|
||||
"""
|
||||
hash = sha3.keccak_256()
|
||||
_hash = sha3.keccak_256()
|
||||
for message in messages:
|
||||
hash.update(message)
|
||||
return hash.digest()
|
||||
_hash.update(message)
|
||||
return _hash.digest()
|
||||
|
||||
|
||||
def ecdsa_sign(message: bytes,
|
||||
privkey: UmbralPrivateKey
|
||||
private_key: UmbralPrivateKey
|
||||
) -> bytes:
|
||||
"""
|
||||
Accepts a hashed message and signs it with the private key given.
|
||||
|
||||
:param message: Message to hash and sign
|
||||
:param privkey: Private key to sign with
|
||||
:param private_key: Private key to sign with
|
||||
|
||||
:return: signature
|
||||
"""
|
||||
signing_key = privkey.to_cryptography_privkey()
|
||||
signing_key = private_key.to_cryptography_privkey()
|
||||
signature_der_bytes = signing_key.sign(message, ec.ECDSA(SHA256))
|
||||
return signature_der_bytes
|
||||
|
||||
|
||||
def verify_eip_191(address: object, message: object, signature: object) -> object:
|
||||
def verify_eip_191(address: str, message: bytes, signature: bytes) -> bool:
|
||||
"""
|
||||
EIP-191 Compatible signature verification for usage with w3.eth.sign.
|
||||
"""
|
||||
|
@ -115,15 +115,12 @@ def verify_eip_191(address: object, message: object, signature: object) -> objec
|
|||
recovered_address = to_checksum_address(recovery)
|
||||
|
||||
signature_is_valid = recovered_address == to_checksum_address(address)
|
||||
if signature_is_valid:
|
||||
return True
|
||||
else:
|
||||
raise InvalidSignature
|
||||
return signature_is_valid
|
||||
|
||||
|
||||
def ecdsa_verify(message: bytes,
|
||||
def verify_ecdsa(message: bytes,
|
||||
signature: bytes,
|
||||
pubkey: UmbralPublicKey
|
||||
public_key: UmbralPublicKey
|
||||
) -> bool:
|
||||
"""
|
||||
Accepts a message and signature and verifies it with the
|
||||
|
@ -131,11 +128,11 @@ def ecdsa_verify(message: bytes,
|
|||
|
||||
:param message: Message to verify
|
||||
:param signature: Signature to verify
|
||||
:param pubkey: UmbralPublicKey to verify signature with
|
||||
:param public_key: UmbralPublicKey to verify signature with
|
||||
|
||||
:return: True if valid, False if invalid.
|
||||
"""
|
||||
cryptography_pub_key = pubkey.to_cryptography_pubkey()
|
||||
cryptography_pub_key = public_key.to_cryptography_pubkey()
|
||||
|
||||
try:
|
||||
cryptography_pub_key.verify(
|
||||
|
@ -170,7 +167,6 @@ def generate_self_signed_certificate(host: str,
|
|||
cert = cert.serial_number(x509.random_serial_number())
|
||||
cert = cert.not_valid_before(now)
|
||||
cert = cert.not_valid_after(now + datetime.timedelta(days=days_valid))
|
||||
# TODO: What are we going to do about domain name here? 179
|
||||
cert = cert.add_extension(x509.SubjectAlternativeName([x509.IPAddress(IPv4Address(host))]), critical=False)
|
||||
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
|
||||
|
||||
|
|
|
@ -35,7 +35,8 @@ from constant_sorrow.constants import (
|
|||
NEVER_SEEN,
|
||||
NO_STORAGE_AVAILIBLE,
|
||||
FLEET_STATES_MATCH,
|
||||
CERTIFICATE_NOT_SAVED
|
||||
CERTIFICATE_NOT_SAVED,
|
||||
UNKNOWN_FLEET_STATE
|
||||
)
|
||||
from cryptography.x509 import Certificate
|
||||
from requests.exceptions import SSLError
|
||||
|
@ -46,7 +47,7 @@ from twisted.logger import Logger
|
|||
|
||||
from nucypher.config.constants import SeednodeMetadata
|
||||
from nucypher.config.storages import ForgetfulNodeStorage
|
||||
from nucypher.crypto.api import keccak_digest, verify_eip_191
|
||||
from nucypher.crypto.api import keccak_digest, verify_eip_191, verify_ecdsa
|
||||
from nucypher.crypto.powers import BlockchainPower, SigningPower, DecryptingPower, NoSigningPower
|
||||
from nucypher.crypto.signing import signature_splitter
|
||||
from nucypher.network import LEARNING_LOOP_VERSION
|
||||
|
@ -710,9 +711,6 @@ class Learner:
|
|||
#
|
||||
|
||||
try:
|
||||
# TODO: Streamline path generation
|
||||
# FIXME: unused name "certificate filepath"
|
||||
certificate_filepath = self.node_storage.generate_certificate_filepath(checksum_address=current_teacher.checksum_public_address)
|
||||
|
||||
response = self.network_middleware.get_nodes_via_rest(node=current_teacher,
|
||||
nodes_i_need=self._node_ids_to_learn_about_immediately,
|
||||
|
@ -796,7 +794,6 @@ class Learner:
|
|||
#
|
||||
|
||||
certificate_filepath = self.node_storage.store_node_certificate(certificate=node.certificate)
|
||||
|
||||
try:
|
||||
if eager:
|
||||
node.verify_node(self.network_middleware,
|
||||
|
@ -807,11 +804,27 @@ class Learner:
|
|||
else:
|
||||
node.validate_metadata(accept_federated_only=self.federated_only) # TODO: 466
|
||||
|
||||
#
|
||||
# Report Failure
|
||||
#
|
||||
|
||||
except NodeSeemsToBeDown as e:
|
||||
self.log.info(f"Can't connect to {node} to verify it right now.")
|
||||
self.log.info(f"Verification Failed - "
|
||||
f"Cannot establish connection to {node}.")
|
||||
|
||||
except node.StampNotSigned:
|
||||
self.log.warn(f'Verification Failed - '
|
||||
f'{node} stamp is unsigned.')
|
||||
|
||||
except node.NotStaking:
|
||||
self.log.warn(f'Verification Failed - '
|
||||
f'{node} has no active stakes in the current period ({self.miner_agent.get_current_period()}')
|
||||
|
||||
except node.InvalidWalletSignature:
|
||||
self.log.warn(f'Verification Failed - '
|
||||
f'{node} has an invalid wallet signature for {node.checksum_public_address}')
|
||||
|
||||
except node.InvalidNode:
|
||||
# TODO: Account for possibility that stamp, rather than interface, was bad.
|
||||
self.log.warn(node.invalid_metadata_message.format(node))
|
||||
|
||||
except node.SuspiciousActivity:
|
||||
|
@ -819,19 +832,25 @@ class Learner:
|
|||
f"Propagated by: {current_teacher}"
|
||||
self.log.warn(message)
|
||||
|
||||
#
|
||||
# Success
|
||||
#
|
||||
|
||||
else:
|
||||
# Success
|
||||
new = self.remember_node(node, record_fleet_state=False)
|
||||
if new:
|
||||
new_nodes.append(node)
|
||||
|
||||
self._adjust_learning(new_nodes)
|
||||
#
|
||||
# Continue
|
||||
#
|
||||
|
||||
self._adjust_learning(new_nodes)
|
||||
learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new."
|
||||
self.log.info(learning_round_log_message.format(self._learning_round,
|
||||
current_teacher,
|
||||
len(node_list),
|
||||
len(new_nodes)), )
|
||||
len(new_nodes)))
|
||||
if new_nodes:
|
||||
self.known_nodes.record_fleet_state()
|
||||
for node in new_nodes:
|
||||
|
@ -840,10 +859,8 @@ class Learner:
|
|||
|
||||
|
||||
class Teacher:
|
||||
|
||||
TEACHER_VERSION = LEARNING_LOOP_VERSION
|
||||
verified_stamp = False
|
||||
verified_interface = False
|
||||
_verified_node = False
|
||||
_interface_info_splitter = (int, 4, {'byteorder': 'big'})
|
||||
log = Logger("teacher")
|
||||
__DEFAULT_MIN_SEED_STAKE = 0
|
||||
|
@ -854,38 +871,63 @@ class Teacher:
|
|||
certificate_filepath: str,
|
||||
interface_signature=NOT_SIGNED.bool_value(False),
|
||||
timestamp=NOT_SIGNED,
|
||||
identity_evidence=NOT_SIGNED,
|
||||
decentralized_identity_evidence=NOT_SIGNED,
|
||||
substantiate_immediately=False,
|
||||
password=None,
|
||||
) -> None:
|
||||
|
||||
#
|
||||
# Fleet
|
||||
#
|
||||
|
||||
self.serving_domains = domains
|
||||
self.certificate = certificate
|
||||
self.certificate_filepath = certificate_filepath
|
||||
self._interface_signature_object = interface_signature
|
||||
self._timestamp = timestamp
|
||||
self.last_seen = NEVER_SEEN("No Connection to Node")
|
||||
self.fleet_state_checksum = None
|
||||
self.fleet_state_updated = None
|
||||
self._identity_evidence = constant_or_bytes(identity_evidence)
|
||||
self.last_seen = NEVER_SEEN("No Connection to Node")
|
||||
|
||||
self.fleet_state_icon = UNKNOWN_FLEET_STATE
|
||||
self.fleet_state_nickname = UNKNOWN_FLEET_STATE
|
||||
self.fleet_state_nickname_metadata = UNKNOWN_FLEET_STATE
|
||||
|
||||
#
|
||||
# Identity
|
||||
#
|
||||
|
||||
self._timestamp = timestamp
|
||||
self.certificate = certificate
|
||||
self.certificate_filepath = certificate_filepath
|
||||
self.__interface_signature = interface_signature
|
||||
self.__decentralized_identity_evidence = constant_or_bytes(decentralized_identity_evidence)
|
||||
|
||||
# Assume unverified
|
||||
self.verified_stamp = False
|
||||
self.verified_worker = False
|
||||
self.verified_interface = False
|
||||
self.verified_node = False
|
||||
|
||||
if substantiate_immediately:
|
||||
self.substantiate_stamp(password=password) # TODO: Derive from keyring
|
||||
self.substantiate_stamp(client_password=password)
|
||||
|
||||
class InvalidNode(SuspiciousActivity):
|
||||
"""
|
||||
Raised when a node has an invalid characteristic - stamp, interface, or address.
|
||||
"""
|
||||
"""Raised when a node has an invalid characteristic - stamp, interface, or address."""
|
||||
|
||||
class InvalidStamp(InvalidNode):
|
||||
"""Base exception class for invalid character stamps"""
|
||||
|
||||
class StampNotSigned(InvalidStamp):
|
||||
"""Raised when a node does not have a stamp signature when one is required for verification"""
|
||||
|
||||
class InvalidWalletSignature(InvalidStamp):
|
||||
"""Raised when a stamp fails signature verification or recovers an unexpected wallet address"""
|
||||
|
||||
class NotStaking(InvalidStamp):
|
||||
"""Raised when a node fails verification because it is not currently staking"""
|
||||
|
||||
class WrongMode(TypeError):
|
||||
"""
|
||||
Raised when a Character tries to use another Character as decentralized when the latter is federated_only.
|
||||
"""
|
||||
"""Raised when a Character tries to use another Character as decentralized when the latter is federated_only."""
|
||||
|
||||
class IsFromTheFuture(TypeError):
|
||||
"""
|
||||
Raised when deserializing a Character from a future version.
|
||||
"""
|
||||
"""Raised when deserializing a Character from a future version."""
|
||||
|
||||
@classmethod
|
||||
def from_tls_hosting_power(cls, tls_hosting_power: TLSHostingPower, *args, **kwargs) -> 'Teacher':
|
||||
|
@ -910,8 +952,17 @@ class Teacher:
|
|||
return sorted(nodes_to_consider, key=lambda n: n.checksum_public_address)
|
||||
|
||||
def update_snapshot(self, checksum, updated, number_of_known_nodes):
|
||||
# We update the simple snapshot here, but of course if we're dealing with an instance that is also a Learner, it has
|
||||
# its own notion of its FleetState, so we probably need a reckoning of sorts here to manage that. In time.
|
||||
"""
|
||||
TODO: We update the simple snapshot here, but of course if we're dealing
|
||||
with an instance that is also a Learner, it has
|
||||
its own notion of its FleetState, so we probably
|
||||
need a reckoning of sorts here to manage that. In time.
|
||||
|
||||
:param checksum:
|
||||
:param updated:
|
||||
:param number_of_known_nodes:
|
||||
:return:
|
||||
"""
|
||||
self.fleet_state_nickname, self.fleet_state_nickname_metadata = nickname_from_seed(checksum, number_of_pairs=1)
|
||||
self.fleet_state_checksum = checksum
|
||||
self.fleet_state_updated = updated
|
||||
|
@ -923,17 +974,16 @@ class Teacher:
|
|||
# Stamp
|
||||
#
|
||||
|
||||
def _stamp_has_valid_wallet_signature(self):
|
||||
"""Offline Signature Verification"""
|
||||
signature_bytes = self._identity_evidence
|
||||
if signature_bytes is NOT_SIGNED:
|
||||
def _stamp_has_valid_wallet_signature(self) -> bool:
|
||||
"""Off-chain Signature Verification of ethereum client signature of stamp"""
|
||||
if self.__decentralized_identity_evidence is NOT_SIGNED:
|
||||
return False
|
||||
signature_is_valid = verify_eip_191(message=bytes(self.stamp),
|
||||
signature=signature_bytes,
|
||||
signature=self.__decentralized_identity_evidence,
|
||||
address=self.checksum_public_address)
|
||||
return signature_is_valid
|
||||
|
||||
def _is_valid_worker(self):
|
||||
def _is_valid_worker(self) -> bool:
|
||||
"""
|
||||
This method assumes the stamp's signature is valid and accurate.
|
||||
As a follow-up, validate the Staker and Worker on-chain.
|
||||
|
@ -941,45 +991,35 @@ class Teacher:
|
|||
TODO: #1033 - Verify Staker <-> Worker relationship on-chain
|
||||
"""
|
||||
locked_tokens = self.miner_agent.get_locked_tokens(miner_address=self.checksum_public_address)
|
||||
if not locked_tokens:
|
||||
raise self.InvalidNode(f"{self.checksum_public_address} has no active stakes.")
|
||||
return locked_tokens > 0
|
||||
|
||||
def stamp_is_valid(self, verify_staking: bool = True) -> bool:
|
||||
"""Offline-only check of valid stamp signature"""
|
||||
def validate_stamp(self, verify_staking: bool = True) -> None:
|
||||
|
||||
#
|
||||
# Federated
|
||||
#
|
||||
|
||||
if self.federated_only:
|
||||
if self._identity_evidence is not NOT_SIGNED:
|
||||
message = "This node cannot be verified in this manner, " \
|
||||
"but is OK to use in federated mode if you " \
|
||||
"have reason to believe it is trustworthy."
|
||||
raise self.WrongMode(message)
|
||||
message = "This node cannot be verified in this manner, " \
|
||||
"but is OK to use in federated mode if you " \
|
||||
"have reason to believe it is trustworthy."
|
||||
raise self.WrongMode(message)
|
||||
|
||||
#
|
||||
# Decentralized
|
||||
#
|
||||
|
||||
else:
|
||||
|
||||
if self._identity_evidence is NOT_SIGNED:
|
||||
raise self.InvalidNode
|
||||
if self.__decentralized_identity_evidence is NOT_SIGNED:
|
||||
raise self.StampNotSigned
|
||||
|
||||
# Off-chain signature verification
|
||||
if self._stamp_has_valid_wallet_signature():
|
||||
self.verified_stamp = True
|
||||
return True
|
||||
if not self._stamp_has_valid_wallet_signature():
|
||||
raise self.InvalidWalletSignature
|
||||
|
||||
# On-chain staking check
|
||||
if verify_staking:
|
||||
self._is_valid_worker()
|
||||
if self._is_valid_worker(): # <-- Blockchain CALL
|
||||
self.verified_worker = True
|
||||
else:
|
||||
raise self.NotStaking
|
||||
|
||||
def verify_id(self, ursula_id, digest_factory=bytes):
|
||||
self.verify()
|
||||
if not ursula_id == digest_factory(self.canonical_public_address):
|
||||
raise self.InvalidNode
|
||||
self.verified_stamp = True
|
||||
|
||||
def validate_metadata(self,
|
||||
accept_federated_only: bool = False,
|
||||
|
@ -987,7 +1027,7 @@ class Teacher:
|
|||
|
||||
# Verify the interface signature
|
||||
if not self.verified_interface:
|
||||
self.interface_is_valid()
|
||||
self.validate_interface()
|
||||
|
||||
# Verify the identity evidence
|
||||
if self.verified_stamp:
|
||||
|
@ -995,7 +1035,7 @@ class Teacher:
|
|||
|
||||
# Offline check of valid stamp signature by worker
|
||||
try:
|
||||
self.stamp_is_valid(verify_staking=verify_staking)
|
||||
self.validate_stamp(verify_staking=verify_staking)
|
||||
except self.WrongMode:
|
||||
if not accept_federated_only:
|
||||
raise
|
||||
|
@ -1021,56 +1061,61 @@ class Teacher:
|
|||
"""
|
||||
|
||||
# Only perform this check once per object
|
||||
if not force:
|
||||
if self._verified_node:
|
||||
return True
|
||||
if not force and self.verified_node:
|
||||
return True
|
||||
|
||||
# This is both the stamp's client signature and interface metadata check.
|
||||
# This is both the stamp's client signature and interface metadata check; May raise InvalidNode
|
||||
self.validate_metadata(accept_federated_only=accept_federated_only)
|
||||
|
||||
# The node's metadata is valid; let's be sure the interface is in order.
|
||||
if not certificate_filepath:
|
||||
|
||||
if self.certificate_filepath is CERTIFICATE_NOT_SAVED:
|
||||
raise TypeError("We haven't saved a certificate for this node yet.")
|
||||
else:
|
||||
certificate_filepath = self.certificate_filepath
|
||||
|
||||
# The node's metadata is valid; let's be sure the interface is in order.
|
||||
response_data = network_middleware.node_information(host=self.rest_information()[0].host,
|
||||
port=self.rest_information()[0].port,
|
||||
certificate_filepath=certificate_filepath)
|
||||
|
||||
version, node_bytes = self.version_splitter(response_data, return_remainder=True)
|
||||
|
||||
node_details = self.internal_splitter(node_bytes)
|
||||
# TODO: #589 - check timestamp here.
|
||||
|
||||
verifying_keys_match = node_details['verifying_key'] == self.public_keys(SigningPower)
|
||||
encrypting_keys_match = node_details['encrypting_key'] == self.public_keys(DecryptingPower)
|
||||
addresses_match = node_details['public_address'] == self.canonical_public_address
|
||||
evidence_matches = node_details['identity_evidence'] == self._identity_evidence
|
||||
evidence_matches = node_details['decentralized_identity_evidence'] == self.__decentralized_identity_evidence
|
||||
|
||||
if not all((encrypting_keys_match, verifying_keys_match, addresses_match, evidence_matches)):
|
||||
# TODO: #355 - Optional reporting.
|
||||
# Failure
|
||||
if not addresses_match:
|
||||
self.log.warn("Wallet address swapped out. It appears that someone is trying to defraud this node.")
|
||||
if not verifying_keys_match:
|
||||
self.log.warn("Verifying key swapped out. It appears that someone is impersonating this node.")
|
||||
raise self.InvalidNode("Wrong cryptographic material for this node - something fishy going on.")
|
||||
else:
|
||||
self._verified_node = True
|
||||
|
||||
def substantiate_stamp(self, password: str):
|
||||
# TODO: #355 - Optional reporting.
|
||||
raise self.InvalidNode("Wrong cryptographic material for this node - something fishy going on.")
|
||||
|
||||
else:
|
||||
# Success
|
||||
self.verified_node = True
|
||||
|
||||
@property
|
||||
def decentralized_identity_evidence(self):
|
||||
return self.__decentralized_identity_evidence
|
||||
|
||||
def substantiate_stamp(self, client_password: str):
|
||||
blockchain_power = self._crypto_power.power_ups(BlockchainPower)
|
||||
blockchain_power.unlock_account(password=password) # TODO: #349
|
||||
blockchain_power.unlock_account(password=client_password) # TODO: #349
|
||||
signature = blockchain_power.sign_message(bytes(self.stamp))
|
||||
self._identity_evidence = signature
|
||||
self.__decentralized_identity_evidence = signature
|
||||
|
||||
#
|
||||
# Interface
|
||||
#
|
||||
|
||||
def interface_is_valid(self) -> bool:
|
||||
def validate_interface(self) -> bool:
|
||||
"""
|
||||
Checks that the interface info is valid for this node's canonical address.
|
||||
"""
|
||||
|
@ -1090,16 +1135,16 @@ class Teacher:
|
|||
def _sign_and_date_interface_info(self):
|
||||
message = self._signable_interface_info_message()
|
||||
self._timestamp = maya.now()
|
||||
self._interface_signature_object = self.stamp(self.timestamp_bytes() + message)
|
||||
self.__interface_signature = self.stamp(self.timestamp_bytes() + message)
|
||||
|
||||
@property
|
||||
def _interface_signature(self):
|
||||
if not self._interface_signature_object:
|
||||
if not self.__interface_signature:
|
||||
try:
|
||||
self._sign_and_date_interface_info()
|
||||
except NoSigningPower:
|
||||
raise NoSigningPower("This Ursula is a stranger and cannot be used to verify.")
|
||||
return self._interface_signature_object
|
||||
return self.__interface_signature
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
|
|
|
@ -71,7 +71,7 @@ def test_actor_with_signing_power_can_sign():
|
|||
|
||||
# ...or to get the signer's public key for verification purposes.
|
||||
# (note: we use the private _der_encoded_bytes here to test directly against the API, instead of Character)
|
||||
verification = api.ecdsa_verify(message, signature._der_encoded_bytes(),
|
||||
verification = api.verify_ecdsa(message, signature._der_encoded_bytes(),
|
||||
stamp_of_the_signer.as_umbral_pubkey())
|
||||
|
||||
assert verification is True
|
||||
|
@ -138,10 +138,10 @@ def test_character_blockchain_power(testerchain, three_agents):
|
|||
assert is_verified is True
|
||||
|
||||
# Test a bad address/pubkey pair
|
||||
with pytest.raises(InvalidSignature):
|
||||
verify_eip_191(address=testerchain.interface.w3.eth.accounts[1],
|
||||
message=data_to_sign,
|
||||
signature=sig)
|
||||
is_verified = verify_eip_191(address=testerchain.interface.w3.eth.accounts[1],
|
||||
message=data_to_sign,
|
||||
signature=sig)
|
||||
assert is_verified is False
|
||||
|
||||
# Test a signature without unlocking the account
|
||||
power.is_unlocked = False
|
||||
|
|
|
@ -53,7 +53,7 @@ def test_new_federated_ursula_announces_herself(ursula_federated_test_config):
|
|||
|
||||
def test_blockchain_ursula_substantiates_stamp(blockchain_ursulas):
|
||||
first_ursula = list(blockchain_ursulas)[0]
|
||||
signature_as_bytes = first_ursula._identity_evidence
|
||||
signature_as_bytes = first_ursula.decentralized_identity_evidence
|
||||
signature_as_bytes = to_standard_signature_bytes(signature_as_bytes)
|
||||
assert verify_eip_191(address=first_ursula.checksum_public_address,
|
||||
message=bytes(first_ursula.stamp),
|
||||
|
@ -68,7 +68,7 @@ def test_blockchain_ursula_verifies_stamp(blockchain_ursulas):
|
|||
|
||||
# This Ursula does not yet have a verified stamp
|
||||
first_ursula.verified_stamp = False
|
||||
first_ursula.stamp_is_valid()
|
||||
first_ursula.validate_stamp()
|
||||
|
||||
# ...but now it's verified.
|
||||
assert first_ursula.verified_stamp
|
||||
|
@ -81,14 +81,15 @@ def test_vladimir_cannot_verify_interface_with_ursulas_signing_key(blockchain_ur
|
|||
# so that Alice (or whomever) pays him instead of Ursula, even though Ursula is providing the service.
|
||||
|
||||
# He finds a target and verifies that its interface is valid.
|
||||
assert his_target.interface_is_valid()
|
||||
assert his_target.validate_interface()
|
||||
|
||||
# Now Vladimir imitates Ursula - copying her public keys and interface info, but inserting his ether address.
|
||||
vladimir = Vladimir.from_target_ursula(his_target, claim_signing_key=True)
|
||||
|
||||
# Vladimir can substantiate the stamp using his own ether address...
|
||||
vladimir.substantiate_stamp(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir.stamp_is_valid()
|
||||
vladimir.substantiate_stamp(client_password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir._is_valid_worker = lambda: True
|
||||
vladimir.validate_stamp()
|
||||
|
||||
# Now, even though his public signing key matches Ursulas...
|
||||
assert vladimir.stamp == his_target.stamp
|
||||
|
@ -96,7 +97,7 @@ def test_vladimir_cannot_verify_interface_with_ursulas_signing_key(blockchain_ur
|
|||
# ...he is unable to pretend that his interface is valid
|
||||
# because the interface validity check contains the canonical public address as part of its message.
|
||||
with pytest.raises(vladimir.InvalidNode):
|
||||
vladimir.interface_is_valid()
|
||||
vladimir.validate_interface()
|
||||
|
||||
# Consequently, the metadata as a whole is also invalid.
|
||||
with pytest.raises(vladimir.InvalidNode):
|
||||
|
@ -109,19 +110,15 @@ def test_vladimir_uses_his_own_signing_key(blockchain_alice, blockchain_ursulas)
|
|||
using his own signing key, which he claims is Ursula's.
|
||||
"""
|
||||
his_target = list(blockchain_ursulas)[4]
|
||||
|
||||
fraudulent_keys = CryptoPower(power_ups=Ursula._default_crypto_powerups) # TODO: Why is this unused?
|
||||
|
||||
vladimir = Vladimir.from_target_ursula(target_ursula=his_target)
|
||||
|
||||
message = vladimir._signable_interface_info_message()
|
||||
signature = vladimir._crypto_power.power_ups(SigningPower).sign(vladimir.timestamp_bytes() + message)
|
||||
vladimir._interface_signature_object = signature
|
||||
|
||||
vladimir.substantiate_stamp(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir._Teacher__interface_signature = signature
|
||||
vladimir.substantiate_stamp(client_password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
# With this slightly more sophisticated attack, his metadata does appear valid.
|
||||
vladimir._is_valid_worker = lambda: True # mock staking verification TODO: Split into two tests
|
||||
vladimir._is_valid_worker = lambda: True # bypass staking verification TODO: Split into two tests
|
||||
vladimir.validate_metadata()
|
||||
|
||||
# However, the actual handshake proves him wrong.
|
||||
|
|
|
@ -10,39 +10,63 @@ from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
|||
from nucypher.utilities.sandbox.ursula import make_federated_ursulas
|
||||
|
||||
|
||||
def test_blockchain_ursula_is_not_valid_with_unsigned_identity_evidence(blockchain_ursulas, caplog):
|
||||
lonely_blockchain_learner, blockchain_teacher, unsigned = list(blockchain_ursulas)[0:3]
|
||||
def test_blockchain_ursula_stamp_verification_tolerance(blockchain_ursulas, caplog):
|
||||
|
||||
unsigned._identity_evidence = NOT_SIGNED
|
||||
#
|
||||
# Setup
|
||||
#
|
||||
|
||||
# Wipe known nodes .
|
||||
lonely_blockchain_learner._Learner__known_nodes = FleetStateTracker()
|
||||
lonely_blockchain_learner._current_teacher_node = blockchain_teacher
|
||||
# TODO: #1035
|
||||
lonely_blockchain_learner, blockchain_teacher, unsigned, *the_others, non_staking_ursula = list(blockchain_ursulas)
|
||||
|
||||
lonely_blockchain_learner.remember_node(blockchain_teacher)
|
||||
warnings = []
|
||||
|
||||
def warning_trapper(event):
|
||||
if event['log_level'] == LogLevel.warn:
|
||||
warnings.append(event)
|
||||
|
||||
#
|
||||
# Attempt to verify unsigned stamp
|
||||
#
|
||||
|
||||
unsigned._Teacher__decentralized_identity_evidence = NOT_SIGNED
|
||||
|
||||
# Wipe known nodes!
|
||||
lonely_blockchain_learner._Learner__known_nodes = FleetStateTracker()
|
||||
lonely_blockchain_learner._current_teacher_node = blockchain_teacher
|
||||
lonely_blockchain_learner.remember_node(blockchain_teacher)
|
||||
|
||||
globalLogPublisher.addObserver(warning_trapper)
|
||||
|
||||
lonely_blockchain_learner.learn_from_teacher_node()
|
||||
|
||||
globalLogPublisher.removeObserver(warning_trapper)
|
||||
|
||||
# We received one warning during learning, and it was about this very matter.
|
||||
assert len(warnings) == 1
|
||||
assert warnings[0]['log_format'] == unsigned.invalid_metadata_message.format(unsigned)
|
||||
|
||||
warning = warnings[0]['log_format']
|
||||
assert str(unsigned) in warning
|
||||
assert "stamp is unsigned" in warning # TODO: Cleanup logging templates
|
||||
assert unsigned not in lonely_blockchain_learner.known_nodes
|
||||
|
||||
# TODO: #1035
|
||||
# minus 3 for self, a non-staking Ursula, and, of course, the unsigned ursula.
|
||||
# minus 3: self, a non-staking ursula, and the unsigned ursula.
|
||||
assert len(lonely_blockchain_learner.known_nodes) == len(blockchain_ursulas) - 3
|
||||
assert blockchain_teacher in lonely_blockchain_learner.known_nodes
|
||||
|
||||
#
|
||||
# Attempt to verify non-staking Ursula
|
||||
#
|
||||
|
||||
lonely_blockchain_learner._current_teacher_node = non_staking_ursula
|
||||
globalLogPublisher.addObserver(warning_trapper)
|
||||
lonely_blockchain_learner.learn_from_teacher_node()
|
||||
globalLogPublisher.removeObserver(warning_trapper)
|
||||
|
||||
assert len(warnings) == 2
|
||||
warning = warnings[1]['log_format']
|
||||
assert str(non_staking_ursula) in warning
|
||||
assert "no active stakes" in warning # TODO: Cleanup logging templates
|
||||
assert non_staking_ursula not in lonely_blockchain_learner.known_nodes
|
||||
|
||||
|
||||
def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog):
|
||||
lonely_ursula_maker = partial(make_federated_ursulas,
|
||||
|
|
|
@ -191,8 +191,8 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali
|
|||
message = vladimir._signable_interface_info_message()
|
||||
signature = vladimir._crypto_power.power_ups(SigningPower).sign(message)
|
||||
|
||||
vladimir.substantiate_stamp(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir._interface_signature_object = signature
|
||||
vladimir.substantiate_stamp(client_password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir._Teacher__interface_signature = signature
|
||||
|
||||
class FakeArrangement:
|
||||
federated = False
|
||||
|
|
Loading…
Reference in New Issue