From d69f3c2c34be784abcd7a47a0a31d472fefba69d Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Wed, 25 Apr 2018 22:08:17 +0200 Subject: [PATCH] Implemented elliptic curve signatures and verify on links, auto proofs on links --- Examples/Echo.py | 2 +- RNS/Identity.py | 11 ++++-- RNS/Interfaces/UdpInterface.py | 8 ++-- RNS/Link.py | 45 +++++++++++++++++++++-- RNS/Packet.py | 67 +++++++++++++++++++++++++++++----- RNS/Resource.py | 3 ++ RNS/Transport.py | 19 +++++++--- 7 files changed, 127 insertions(+), 28 deletions(-) diff --git a/Examples/Echo.py b/Examples/Echo.py index c90d97e..b9d9a00 100644 --- a/Examples/Echo.py +++ b/Examples/Echo.py @@ -138,7 +138,7 @@ def client(destination_hexhash, configpath, timeout=None): # a callback function, that will get called if # the packet times out. if timeout != None: - packet_receipt.setTimeout(timeout) + packet_receipt.set_timeout(timeout) packet_receipt.timeout_callback(packet_timed_out) # We can then set a delivery callback on the receipt. diff --git a/RNS/Identity.py b/RNS/Identity.py index d078eef..8a614ae 100644 --- a/RNS/Identity.py +++ b/RNS/Identity.py @@ -56,10 +56,13 @@ class Identity: @staticmethod def loadKnownDestinations(): if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"): - file = open(RNS.Reticulum.storagepath+"/known_destinations","r") - Identity.known_destinations = umsgpack.load(file) - file.close() - RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destinations from storage", RNS.LOG_VERBOSE) + try: + file = open(RNS.Reticulum.storagepath+"/known_destinations","r") + Identity.known_destinations = umsgpack.load(file) + file.close() + RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destinations from storage", RNS.LOG_VERBOSE) + except: + RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR) else: RNS.log("Destinations file does not exist, so no known destinations loaded", RNS.LOG_VERBOSE) diff --git a/RNS/Interfaces/UdpInterface.py b/RNS/Interfaces/UdpInterface.py index 2b8e2b0..44b4583 100755 --- a/RNS/Interfaces/UdpInterface.py +++ b/RNS/Interfaces/UdpInterface.py @@ -2,19 +2,16 @@ from Interface import Interface import SocketServer import threading import socket +import time import sys import RNS class UdpInterface(Interface): - bind_ip = None - bind_port = None - forward_ip = None - forward_port = None - owner = None def __init__(self, owner, name, bindip=None, bindport=None, forwardip=None, forwardport=None): self.IN = True self.OUT = False + self.transmit_delay = 0.001 self.name = name @@ -42,6 +39,7 @@ class UdpInterface(Interface): self.owner.inbound(data, self) def processOutgoing(self,data): + time.sleep(self.transmit_delay) udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) udp_socket.sendto(data, (self.forward_ip, self.forward_port)) diff --git a/RNS/Link.py b/RNS/Link.py index 11b44a8..0372706 100644 --- a/RNS/Link.py +++ b/RNS/Link.py @@ -30,7 +30,7 @@ class Link: # but calculated from something like # first-hop RTT latency and distance DEFAULT_TIMEOUT = 5 - TIMEOUT_FACTOR = 3 + TIMEOUT_FACTOR = 5 KEEPALIVE = 120 PENDING = 0x00 @@ -54,6 +54,7 @@ class Link: try: link = Link(owner = owner, peer_pub_bytes = data[:Link.ECPUBSIZE]) link.setLinkID(packet) + link.destination = packet.destination RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE) link.handshake() link.attached_interface = packet.receiving_interface @@ -137,7 +138,8 @@ class Link: def loadPeer(self, peer_pub_bytes): self.peer_pub_bytes = peer_pub_bytes self.peer_pub = serialization.load_der_public_key(peer_pub_bytes, backend=default_backend()) - self.peer_pub.curce = Link.CURVE + if not hasattr(self.peer_pub, "curve"): + self.peer_pub.curve = Link.CURVE def setLinkID(self, packet): self.link_id = RNS.Identity.truncatedHash(packet.raw) @@ -162,6 +164,18 @@ class Link: proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF) proof.send() + def prove_packet(self, packet): + signature = self.sign(packet.packet_hash) + # TODO: Hardcoded as explicit proof for now + # if RNS.Reticulum.should_use_implicit_proof(): + # proof_data = signature + # else: + # proof_data = packet.packet_hash + signature + proof_data = packet.packet_hash + signature + + proof = RNS.Packet(self, proof_data, RNS.Packet.PROOF) + proof.send() + def validateProof(self, packet): if self.initiator: peer_pub_bytes = packet.data[:Link.ECPUBSIZE] @@ -326,8 +340,15 @@ class Link: if packet.packet_type == RNS.Packet.DATA: if packet.context == RNS.Packet.NONE: plaintext = self.decrypt(packet.data) - if (self.callbacks.packet != None): + if self.callbacks.packet != None: self.callbacks.packet(plaintext, packet) + + if self.destination.proof_strategy == RNS.Destination.PROVE_ALL: + packet.prove() + + elif self.destination.proof_strategy == RNS.Destination.PROVE_APP: + if self.destination.callbacks.proof_requested: + self.destination.callbacks.proof_requested(packet) elif packet.context == RNS.Packet.LRRTT: if not self.initiator: @@ -415,6 +436,16 @@ class Link: except Exception as e: RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) + def sign(self, message): + return self.prv.sign(message, ec.ECDSA(hashes.SHA256())) + + def validate(self, signature, message): + try: + self.peer_pub.verify(signature, message, ec.ECDSA(hashes.SHA256())) + return True + except Exception as e: + return False + def link_established_callback(self, callback): self.callbacks.link_established = callback @@ -432,7 +463,13 @@ class Link: def resource_concluded_callback(self, callback): self.callbacks.resource_concluded = callback - def setResourceStrategy(self, resource_strategy): + def resource_concluded(self, resource): + if resource in self.incoming_resources: + self.incoming_resources.remove(resource) + if resource in self.outgoing_resources: + self.outgoing_resources.remove(resource) + + def set_resource_strategy(self, resource_strategy): if not resource_strategy in Link.resource_strategies: raise TypeError("Unsupported resource strategy") else: diff --git a/RNS/Packet.py b/RNS/Packet.py index 15d16a2..38321ea 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -17,7 +17,7 @@ class Packet: HEADER_4 = 0x03 # Reserved header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4] - # Context types + # Data packet context types NONE = 0x00 # Generic data packet RESOURCE = 0x01 # Packet is part of a resource RESOURCE_ADV = 0x02 # Packet is a resource advertisement @@ -31,8 +31,9 @@ class Packet: RESPONSE = 0x0A # Packet is a response to a request COMMAND = 0x0B # Packet is a command COMMAND_STATUS = 0x0C # Packet is a status of an executed command - KEEPALIVE = 0xFC # Packet is a keepalive packet - LINKCLOSE = 0xFD # Packet is a link close message + KEEPALIVE = 0xFB # Packet is a keepalive packet + LINKCLOSE = 0xFC # Packet is a link close message + LINKPROOF = 0xFD # Packet is a link packet proof LRRTT = 0xFE # Packet is a link request round-trip time measurement LRPROOF = 0xFF # Packet is a link request proof @@ -102,6 +103,9 @@ class Packet: elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF: # Resource proofs are not encrypted self.ciphertext = self.data + elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK: + # Packet proofs over links are not encrypted + self.ciphertext = self.data elif self.context == Packet.RESOURCE: # A resource takes care of symmetric # encryption by itself @@ -187,9 +191,13 @@ class Packet: raise IOError("Packet was not sent yet") def prove(self, destination=None): - if self.fromPacked and self.destination: + if self.fromPacked and hasattr(self, "destination") and self.destination: if self.destination.identity and self.destination.identity.prv: self.destination.identity.prove(self, destination) + elif self.fromPacked and hasattr(self, "link") and self.link: + self.link.prove_packet(self) + else: + RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR) # Generates a special destination that allows Reticulum # to direct the proof back to the proved packet's sender @@ -246,7 +254,48 @@ class PacketReceipt: # Validate a proof packet def validateProofPacket(self, proof_packet): - return self.validateProof(proof_packet.data) + if hasattr(proof_packet, "link") and proof_packet.link: + return self.validate_link_proof(proof_packet.data, proof_packet.link) + else: + return self.validateProof(proof_packet.data) + + # Validate a raw proof for a link + def validate_link_proof(self, proof, link): + # TODO: Hardcoded as explicit proofs for now + if True or len(proof) == PacketReceipt.EXPL_LENGTH: + # This is an explicit proof + proof_hash = proof[:RNS.Identity.HASHLENGTH/8] + signature = proof[RNS.Identity.HASHLENGTH/8:RNS.Identity.HASHLENGTH/8+RNS.Identity.SIGLENGTH/8] + if proof_hash == self.hash: + proof_valid = link.validate(signature, self.hash) + if proof_valid: + self.status = PacketReceipt.DELIVERED + self.proved = True + self.concluded_at = time.time() + if self.callbacks.delivery != None: + self.callbacks.delivery(self) + return True + else: + return False + else: + return False + elif len(proof) == PacketReceipt.IMPL_LENGTH: + pass + # signature = proof[:RNS.Identity.SIGLENGTH/8] + # proof_valid = self.link.validate(signature, self.hash) + # if proof_valid: + # self.status = PacketReceipt.DELIVERED + # self.proved = True + # self.concluded_at = time.time() + # if self.callbacks.delivery != None: + # self.callbacks.delivery(self) + # RNS.log("valid") + # return True + # else: + # RNS.log("invalid") + # return False + else: + return False # Validate a raw proof def validateProof(self, proof): @@ -286,11 +335,11 @@ class PacketReceipt: def rtt(self): return self.concluded_at - self.sent_at - def isTimedOut(self): + def is_timed_out(self): return (self.sent_at+self.timeout < time.time()) - def checkTimeout(self): - if self.isTimedOut(): + def check_timeout(self): + if self.is_timed_out(): self.status = PacketReceipt.FAILED self.concluded_at = time.time() if self.callbacks.timeout: @@ -298,7 +347,7 @@ class PacketReceipt: # Set the timeout in seconds - def setTimeout(self, timeout): + def set_timeout(self, timeout): self.timeout = float(timeout) # Set a function that gets called when diff --git a/RNS/Resource.py b/RNS/Resource.py index a4edf87..12fcfd6 100644 --- a/RNS/Resource.py +++ b/RNS/Resource.py @@ -311,6 +311,7 @@ class Resource: self.status = Resource.CORRUPT if self.callback != None: + self.link.resource_concluded(self) self.callback(self) @@ -327,6 +328,7 @@ class Resource: if proof_data[RNS.Identity.HASHLENGTH/8:] == self.expected_proof: self.status = Resource.COMPLETE if self.callback != None: + self.link.resource_concluded(self) self.callback(self) else: pass @@ -487,6 +489,7 @@ class Resource: self.link.cancel_incoming_resource(self) if self.callback != None: + self.link.resource_concluded(self) self.callback(self) def progress_callback(self, callback): diff --git a/RNS/Transport.py b/RNS/Transport.py index b371c16..dd5da95 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -73,7 +73,7 @@ class Transport: # Process receipts list for timed-out packets if Transport.receipts_last_checked+Transport.receipts_check_interval < time.time(): for receipt in Transport.receipts: - receipt.checkTimeout() + receipt.check_timeout() if receipt.status != RNS.PacketReceipt.SENT: Transport.receipts.remove(receipt) @@ -127,6 +127,7 @@ class Transport: @staticmethod def packet_filter(packet): + # TODO: Think long and hard about this if packet.context == RNS.Packet.KEEPALIVE: return True if packet.context == RNS.Packet.RESOURCE_REQ: @@ -184,9 +185,10 @@ class Transport: if destination.proof_strategy == RNS.Destination.PROVE_ALL: packet.prove() - if destination.proof_strategy == RNS.Destination.PROVE_APP: + elif destination.proof_strategy == RNS.Destination.PROVE_APP: if destination.callbacks.proof_requested: - destination.callbacks.proof_requested(packet) + if destination.callbacks.proof_requested(packet): + packet.prove() elif packet.packet_type == RNS.Packet.PROOF: if packet.context == RNS.Packet.LRPROOF: @@ -200,6 +202,13 @@ class Transport: if link.link_id == packet.destination_hash: link.receive(packet) else: + if packet.destination_type == RNS.Destination.LINK: + for link in Transport.active_links: + if link.link_id == packet.destination_hash: + packet.link = link + # plaintext = link.decrypt(packet.data) + + # TODO: Make sure everything uses new proof handling if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH: proof_hash = packet.data[:RNS.Identity.HASHLENGTH/8] @@ -230,7 +239,7 @@ class Transport: @staticmethod def registerLink(link): - RNS.log("Registering link "+str(link)) + RNS.log("Registering link "+str(link), RNS.LOG_DEBUG) if link.initiator: Transport.pending_links.append(link) else: @@ -238,7 +247,7 @@ class Transport: @staticmethod def activateLink(link): - RNS.log("Activating link "+str(link)) + RNS.log("Activating link "+str(link), RNS.LOG_DEBUG) if link in Transport.pending_links: Transport.pending_links.remove(link) Transport.active_links.append(link)