Proof handling

This commit is contained in:
Mark Qvist 2018-04-17 17:46:48 +02:00
parent dedea6ba11
commit 19d9b1a4a5
9 changed files with 283 additions and 58 deletions

3
.gitignore vendored
View File

@ -1,5 +1,4 @@
.DS_Store .DS_Store
*.pyc *.pyc
t.py testutils
t2.py
TODO TODO

View File

@ -13,7 +13,7 @@ class Callbacks:
def __init__(self): def __init__(self):
self.link_established = None self.link_established = None
self.packet = None self.packet = None
self.proof = None self.proof_requested = None
class Destination: class Destination:
KEYSIZE = RNS.Identity.KEYSIZE; KEYSIZE = RNS.Identity.KEYSIZE;
@ -101,8 +101,8 @@ class Destination:
def packet_callback(self, callback): def packet_callback(self, callback):
self.callbacks.packet = callback self.callbacks.packet = callback
def proof_callback(self, callback): def proof_requested_callback(self, callback):
self.callbacks.proof = callback self.callbacks.proof_requested = callback
def setProofStrategy(self, proof_strategy): def setProofStrategy(self, proof_strategy):
if not proof_strategy in Destination.proof_strategies: if not proof_strategy in Destination.proof_strategies:

View File

@ -14,12 +14,14 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
class Identity: class Identity:
#KEYSIZE = 1536 #KEYSIZE = 1536
KEYSIZE = 1024 KEYSIZE = 1024
DERKEYSIZE = KEYSIZE+272 DERKEYSIZE = KEYSIZE+272
# Padding size, not configurable # Non-configurable constants
PADDINGSIZE= 336 PADDINGSIZE = 336 # In bits
HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE
# Storage # Storage
known_destinations = {} known_destinations = {}
@ -257,13 +259,21 @@ class Identity:
hashes.SHA256() hashes.SHA256()
) )
return True return True
except: except Exception as e:
return False return False
else: else:
raise KeyError("Signature validation failed because identity does not hold a public key") raise KeyError("Signature validation failed because identity does not hold a public key")
def prove(self, packet, destination): def prove(self, packet, destination=None):
proof_data = packet.packet_hash + self.sign(packet.packet_hash) signature = self.sign(packet.packet_hash)
if RNS.Reticulum.should_use_implicit_proof():
proof_data = signature
else:
proof_data = packet.packet_hash + signature
if destination == None:
destination = packet.generateProofDestination()
proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF) proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF)
proof.send() proof.send()

View File

@ -28,7 +28,6 @@ class AX25():
CRC_CORRECT = chr(0xF0)+chr(0xB8) CRC_CORRECT = chr(0xF0)+chr(0xB8)
# TODO: THIS CLASS IS NOT YET IMPLEMENTED --- PLACEHOLDER ONLY ---
class AX25KISSInterface(Interface): class AX25KISSInterface(Interface):
MAX_CHUNK = 32768 MAX_CHUNK = 32768

View File

@ -2,6 +2,96 @@ import struct
import time import time
import RNS import RNS
class PacketReceipt:
# Receipt status constants
FAILED = 0x00
SENT = 0x01
DELIVERED = 0x02
EXPL_LENGTH = RNS.Identity.HASHLENGTH/8+RNS.Identity.SIGLENGTH/8
IMPL_LENGTH = RNS.Identity.SIGLENGTH/8
# Creates a new packet receipt from a sent packet
def __init__(self, packet):
self.hash = packet.getHash()
self.sent = True
self.sent_at = time.time()
self.timeout = Packet.TIMEOUT
self.proved = False
self.status = PacketReceipt.SENT
self.destination = packet.destination
self.callbacks = PacketReceiptCallbacks()
self.concluded_at = None
# Validate a proof packet
def validateProofPacket(self, proof_packet):
return self.validateProof(proof_packet.data)
# Validate a raw proof
def validateProof(self, proof):
if 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 = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
# This is an implicit proof
signature = proof[:RNS.Identity.SIGLENGTH/8]
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
def isTimedOut(self):
return (self.sent_at+self.timeout < time.time())
def checkTimeout(self):
if self.isTimedOut():
self.status = PacketReceipt.FAILED
self.concluded_at = time.time()
if self.callbacks.timeout:
self.callbacks.timeout(self)
# Set the timeout in seconds
def setTimeout(self, timeout):
self.timeout = float(timeout)
# Set a function that gets called when
# a successfull delivery has been proved
def delivery_callback(self, callback):
self.callbacks.delivery = callback
# Set a function that gets called if the
# delivery times out
def timeout_callback(self, callback):
self.callbacks.timeout = callback
class PacketReceiptCallbacks:
def __init__(self):
self.delivery = None
self.timeout = None
class Packet: class Packet:
# Constants # Constants
DATA = 0x00; DATA = 0x00;
@ -16,6 +106,9 @@ class Packet:
HEADER_4 = 0x03; # Reserved HEADER_4 = 0x03; # Reserved
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4] header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
# Defaults
TIMEOUT = 3600.0
def __init__(self, destination, data, packet_type = DATA, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None): def __init__(self, destination, data, packet_type = DATA, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None):
if destination != None: if destination != None:
if transport_type == None: if transport_type == None:
@ -35,6 +128,7 @@ class Packet:
self.raw = None self.raw = None
self.packed = False self.packed = False
self.sent = False self.sent = False
self.receipt = None
self.fromPacked = False self.fromPacked = False
else: else:
self.raw = data self.raw = data
@ -67,6 +161,7 @@ class Packet:
self.ciphertext = self.destination.encrypt(self.data) self.ciphertext = self.destination.encrypt(self.data)
else: else:
self.ciphertext = self.data self.ciphertext = self.data
if self.header_type == Packet.HEADER_3: if self.header_type == Packet.HEADER_3:
self.header += self.destination.link_id self.header += self.destination.link_id
self.ciphertext = self.data self.ciphertext = self.data
@ -103,10 +198,11 @@ class Packet:
if not self.packed: if not self.packed:
self.pack() self.pack()
RNS.Transport.outbound(self) if RNS.Transport.outbound(self):
self.packet_hash = RNS.Identity.fullHash(self.raw) return self.receipt
self.sent_at = time.time() else:
self.sent = True # TODO: Don't raise error here, handle gracefully
raise IOError("Packet could not be sent! Do you have any outbound interfaces configured?")
else: else:
raise IOError("Packet was already sent") raise IOError("Packet was already sent")
@ -116,21 +212,36 @@ class Packet:
else: else:
raise IOError("Packet was not sent yet") raise IOError("Packet was not sent yet")
def prove(self, destination): def prove(self, destination=None):
if self.fromPacked and self.destination: if self.fromPacked and self.destination:
if self.destination.identity and self.destination.identity.prv: if self.destination.identity and self.destination.identity.prv:
self.destination.identity.prove(self, destination) self.destination.identity.prove(self, destination)
# Generates a special destination that allows Reticulum
# to direct the proof back to the proved packet's sender
def generateProofDestination(self):
return ProofDestination(self)
def validateProofPacket(self, proof_packet): def validateProofPacket(self, proof_packet):
return self.validateProof(proof_packet.data) return self.receipt.validateProofPacket(proof_packet)
def validateProof(self, proof): def validateProof(self, proof):
proof_hash = proof[:32] return self.receipt.validateProof(proof)
signature = proof[32:]
if proof_hash == self.packet_hash:
return self.destination.identity.validate(signature, proof_hash)
else:
return False
def updateHash(self):
self.packet_hash = self.getHash()
def getHash(self):
return RNS.Identity.fullHash(self.getHashablePart())
def getHashablePart(self):
return self.raw[0:1]+self.raw[2:]
class ProofDestination:
def __init__(self, packet):
self.hash = packet.getHash()[:10];
self.type = RNS.Destination.SINGLE
def encrypt(self, plaintext):
return plaintext

View File

@ -29,6 +29,7 @@ class Reticulum:
Reticulum.cachepath = Reticulum.configdir+"/storage/cache" Reticulum.cachepath = Reticulum.configdir+"/storage/cache"
Reticulum.__allow_unencrypted = False Reticulum.__allow_unencrypted = False
Reticulum.__use_implicit_proof = False
if not os.path.isdir(Reticulum.storagepath): if not os.path.isdir(Reticulum.storagepath):
os.makedirs(Reticulum.storagepath) os.makedirs(Reticulum.storagepath)
@ -50,6 +51,8 @@ class Reticulum:
RNS.Identity.loadKnownDestinations() RNS.Identity.loadKnownDestinations()
Reticulum.router = self Reticulum.router = self
RNS.Transport.scheduleJobs()
atexit.register(RNS.Identity.exitHandler) atexit.register(RNS.Identity.exitHandler)
def applyConfig(self): def applyConfig(self):
@ -66,6 +69,11 @@ class Reticulum:
if "reticulum" in self.config: if "reticulum" in self.config:
for option in self.config["reticulum"]: for option in self.config["reticulum"]:
value = self.config["reticulum"][option] value = self.config["reticulum"][option]
if option == "use_implicit_proof":
if value == "true":
Reticulum.__use_implicit_proof = True
if value == "false":
Reticulum.__use_implicit_proof = False
if option == "allow_unencrypted": if option == "allow_unencrypted":
if value == "true": if value == "true":
RNS.log("", RNS.LOG_CRITICAL) RNS.log("", RNS.LOG_CRITICAL)
@ -259,4 +267,8 @@ class Reticulum:
@staticmethod @staticmethod
def should_allow_unencrypted(): def should_allow_unencrypted():
return Reticulum.__allow_unencrypted return Reticulum.__allow_unencrypted
@staticmethod
def should_use_implicit_proof():
return Reticulum.__use_implicit_proof

View File

@ -1,4 +1,7 @@
import RNS import RNS
import time
import threading
from time import sleep
class Transport: class Transport:
# Constants # Constants
@ -8,15 +11,65 @@ class Transport:
TUNNEL = 0x03; TUNNEL = 0x03;
types = [BROADCAST, TRANSPORT, RELAY, TUNNEL] types = [BROADCAST, TRANSPORT, RELAY, TUNNEL]
interfaces = [] interfaces = [] # All active interfaces
destinations = [] destinations = [] # All active destinations
pending_links = [] pending_links = [] # Links that are being established
active_links = [] active_links = [] # Links that are active
packet_hashlist = [] packet_hashlist = [] # A list of packet hashes for duplicate detection
receipts = [] # Receipts of all outgoing packets for proof processing
jobs_locked = False
jobs_running = False
job_interval = 0.250
receipts_last_checked = 0.0
receipts_check_interval = 1.0
hashlist_maxsize = 1000000
@staticmethod
def scheduleJobs():
thread = threading.Thread(target=Transport.jobloop)
thread.setDaemon(True)
thread.start()
@staticmethod
def jobloop():
while (True):
Transport.jobs()
sleep(Transport.job_interval)
@staticmethod
def jobs():
Transport.jobs_running = True
try:
if not Transport.jobs_locked:
# 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()
if receipt.status != RNS.PacketReceipt.SENT:
Transport.receipts.remove(receipt)
Transport.receipts_last_checked = time.time()
# Cull the packet hashlist if it has reached max size
while (len(Transport.packet_hashlist) > Transport.hashlist_maxsize):
Transport.packet_hashlist.pop(0)
except Exception as e:
RNS.log("An exception occurred while running Transport jobs.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
Transport.jobs_running = False
@staticmethod @staticmethod
def outbound(packet): def outbound(packet):
Transport.cacheRaw(packet.raw) while (Transport.jobs_running):
sleep(0.1)
Transport.jobs_locked = True
packet.updateHash()
sent = False
for interface in Transport.interfaces: for interface in Transport.interfaces:
if interface.OUT: if interface.OUT:
should_transmit = True should_transmit = True
@ -27,19 +80,38 @@ class Transport:
if should_transmit: if should_transmit:
RNS.log("Transmitting "+str(len(packet.raw))+" bytes via: "+str(interface), RNS.LOG_DEBUG) RNS.log("Transmitting "+str(len(packet.raw))+" bytes via: "+str(interface), RNS.LOG_DEBUG)
interface.processOutgoing(packet.raw) interface.processOutgoing(packet.raw)
sent = True
if sent:
packet.sent = True
packet.sent_at = time.time()
if (packet.packet_type == RNS.Packet.DATA):
packet.receipt = RNS.PacketReceipt(packet)
Transport.receipts.append(packet.receipt)
Transport.cache(packet)
Transport.jobs_locked = False
return sent
@staticmethod @staticmethod
def inbound(raw, interface=None): def inbound(raw, interface=None):
packet_hash = RNS.Identity.fullHash(raw) while (Transport.jobs_running):
RNS.log(str(interface)+" received packet with hash "+RNS.prettyhexrep(packet_hash), RNS.LOG_DEBUG) sleep(0.1)
Transport.jobs_locked = True
packet = RNS.Packet(None, raw)
packet.unpack()
packet.updateHash()
packet.receiving_interface = interface
if not packet_hash in Transport.packet_hashlist: RNS.log(str(interface)+" received packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_DEBUG)
Transport.packet_hashlist.append(packet_hash)
packet = RNS.Packet(None, raw)
packet.unpack()
packet.packet_hash = packet_hash
packet.receiving_interface = interface
if not packet.packet_hash in Transport.packet_hashlist:
Transport.packet_hashlist.append(packet.packet_hash)
if packet.packet_type == RNS.Packet.ANNOUNCE: if packet.packet_type == RNS.Packet.ANNOUNCE:
if RNS.Identity.validateAnnounce(packet): if RNS.Identity.validateAnnounce(packet):
Transport.cache(packet) Transport.cache(packet)
@ -64,6 +136,13 @@ class Transport:
destination.receive(packet) destination.receive(packet)
Transport.cache(packet) Transport.cache(packet)
if destination.proof_strategy == RNS.Destination.PROVE_ALL:
packet.prove()
if destination.proof_strategy == RNS.Destination.PROVE_APP:
if destination.callbacks.proof_requested:
destination.callbacks.proof_requested(packet)
if packet.packet_type == RNS.Packet.PROOF: if packet.packet_type == RNS.Packet.PROOF:
if packet.header_type == RNS.Packet.HEADER_3: if packet.header_type == RNS.Packet.HEADER_3:
# This is a link request proof, forward # This is a link request proof, forward
@ -72,11 +151,27 @@ class Transport:
if link.link_id == packet.destination_hash: if link.link_id == packet.destination_hash:
link.validateProof(packet) link.validateProof(packet)
else: else:
for destination in Transport.destinations: # TODO: Make sure everything uses new proof handling
if destination.hash == packet.destination_hash: if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH:
if destination.proofcallback != None: proof_hash = packet.data[:RNS.Identity.HASHLENGTH/8]
destination.proofcallback(packet) else:
# TODO: add universal proof handling proof_hash = None
for receipt in Transport.receipts:
receipt_validated = False
if proof_hash != None:
# Only test validation if hash matches
if receipt.hash == proof_hash:
receipt_validated = receipt.validateProofPacket(packet)
else:
# In case of an implicit proof, we have
# to check every single outstanding receipt
receipt_validated = receipt.validateProofPacket(packet)
if receipt_validated:
Transport.receipts.remove(receipt)
Transport.jobs_locked = False
@staticmethod @staticmethod
def registerDestination(destination): def registerDestination(destination):
@ -112,15 +207,13 @@ class Transport:
@staticmethod @staticmethod
def cache(packet): def cache(packet):
if RNS.Transport.shouldCache(packet): if RNS.Transport.shouldCache(packet):
RNS.Transport.cacheRaw(packet.raw) try:
packet_hash = RNS.hexrep(packet.getHash(), delimit=False)
file = open(RNS.Reticulum.cachepath+"/"+packet_hash, "w")
file.write(packet.raw)
file.close()
RNS.log("Wrote packet "+packet_hash+" to cache", RNS.LOG_DEBUG)
except Exception as e:
RNS.log("Error writing packet to cache", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e))
@staticmethod
def cacheRaw(raw):
try:
file = open(RNS.Reticulum.cachepath+"/"+RNS.hexrep(RNS.Identity.fullHash(raw), delimit=False), "w")
file.write(raw)
file.close()
RNS.log("Wrote packet "+RNS.prettyhexrep(RNS.Identity.fullHash(raw))+" to cache", RNS.LOG_DEBUG)
except Exception as e:
RNS.log("Error writing packet to cache", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e))

View File

@ -9,6 +9,7 @@ from .Link import Link
from .Transport import Transport from .Transport import Transport
from .Destination import Destination from .Destination import Destination
from .Packet import Packet from .Packet import Packet
from .Packet import PacketReceipt
modules = glob.glob(os.path.dirname(__file__)+"/*.py") modules = glob.glob(os.path.dirname(__file__)+"/*.py")
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')] __all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]

View File

@ -38,7 +38,7 @@ BOMS = {
BOM_UTF16: ('utf_16', 'utf_16'), BOM_UTF16: ('utf_16', 'utf_16'),
} }
# All legal variants of the BOM codecs. # All legal variants of the BOM codecs.
# TODO: the list of aliases is not meant to be exhaustive, is there a # The list of aliases is not meant to be exhaustive, is there a
# better way ? # better way ?
BOM_LIST = { BOM_LIST = {
'utf_16': 'utf_16', 'utf_16': 'utf_16',