From 36c7cb9e5113eafffaa3b165e422f9026c60ea58 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= <tuxmain@zettascript.org>
Date: Sat, 26 Oct 2019 20:52:58 +0200
Subject: [PATCH] Add /idtysig

---
 README.md |  3 +++
 client.py | 35 ++++++++++++++++++++++++++++++-----
 server.py | 52 +++++++++++++++++++++++++++++++++++-----------------
 utils.py  | 26 +++++++++++++++++++++-----
 4 files changed, 89 insertions(+), 27 deletions(-)

diff --git a/README.md b/README.md
index 1b0c1e6..7ff5c52 100644
--- a/README.md
+++ b/README.md
@@ -207,6 +207,9 @@ Command: **/getconfirm/_sender\_pubkey_/_in\_seed1_** get mix confirmation
 		CONFIRMS // Each confirm is preceded by its length, as above
 	)
 
+Command: **/idtysig** update identity signature
+Content: Identity signature (must be signed by current server idty)
+
 Command: **/mix/_sender_/_amount_/_base_[/client]** mix a transaction (set "client" option if request is sent to entry node)  
 * If "client": _sender_ is the sender pubkey
 * Else: _sender_ is the sender peer hash
diff --git a/client.py b/client.py
index b72594f..4735b65 100644
--- a/client.py
+++ b/client.py
@@ -350,10 +350,14 @@ Options:
  --onion           use proxy only when connecting to .onion
  -d <path>         config directory (default ~/.config/gmixer-client)
  --no-tx           Do not send transaction (for debug)
+ --idtysig <pubkey> <host> <port>
+                   Update node's identity signature (no mixing)
 
 Example:
 python3 client.py -h svetsae7j3usrycn.onion 10951 -r 78ZwwgpgdH5uLZLbThUQH7LKwPgjMunYfLiCfUCySkM8 -p 127.0.0.1 9050 --onion
- ⤷	send 10Ğ1 to Duniter developers
+ ⤷	send 10Ğ1 to Duniter developers (through a Tor peer)
+python3 client.py -h txmn.tk 10951 -r HVXB7mrnrLDfJVALZiZknFg8FgPPi5k4y1md6GCLYGEK -a 5000
+ ⤷	send 50Ğ1 to ĞMixer contributors
 """)
 		exit()
 	
@@ -364,6 +368,31 @@ python3 client.py -h svetsae7j3usrycn.onion 10951 -r 78ZwwgpgdH5uLZLbThUQH7LKwPg
 	
 	conf = read_config(DIR)
 	
+	proxy = None
+	if "-p" in sys.argv:
+		proxy = (utils.getargv("-p", "127.0.0.1", 1), int(utils.getargv("-p", 9050, 2)))
+	proxy_onion_only = "--onion" in sys.argv
+	
+	if "--idtysig" in sys.argv:
+		loop = True
+		while loop:
+			idty_keys = SigningKey.from_credentials(
+				getpass.getpass("Identity passphrase (salt):"),
+				getpass.getpass("Identity password:")
+			)
+			print(idty_keys.pubkey)
+			loop = input("Is that the right pubkey? [yn]: ").lower() != "y"
+		
+		print(utils.sdata(
+			(utils.getargv("--idtysig", n=2), int(utils.getargv("--idtysig", n=3))),
+			"POST",
+			"/idtysig",
+			utils.gen_idty_sig(PublicKey(utils.getargv("--idtysig", n=1)), idty_keys)[0],
+			proxy=proxy,
+			proxy_onion_only=proxy_onion_only
+		))
+		exit()
+	
 	receiver = utils.getargv("-r", "")
 	if receiver == "":
 		print("Error: No receiver set (try option --help)")
@@ -375,10 +404,6 @@ python3 client.py -h svetsae7j3usrycn.onion 10951 -r 78ZwwgpgdH5uLZLbThUQH7LKwPg
 	port = int(utils.getargv("-h", "", 2))
 	amount = int(utils.getargv("-a", "1000"))
 	layers = int(utils.getargv("-l", "3"))
-	proxy = None
-	if "-p" in sys.argv:
-		proxy = (utils.getargv("-p", "127.0.0.1", 1), int(utils.getargv("-p", 9050, 2)))
-	proxy_onion_only = "--onion" in sys.argv
 	send_tx = not "--no-tx" in sys.argv
 	
 	db_txs = plyvel.DB(DIR+"/client_db_txs", create_if_missing=True)
diff --git a/server.py b/server.py
index 9d15b5f..034630d 100644
--- a/server.py
+++ b/server.py
@@ -221,7 +221,7 @@ def read_config(cdir, conf_overwrite={}):
 	return conf
 
 class ServerThread(Thread):
-	def __init__(self, conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs):
+	def __init__(self, conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs, timers, client_th):
 		Thread.__init__(self)
 		
 		self.conf = conf
@@ -232,6 +232,8 @@ class ServerThread(Thread):
 		self.tx_in_index = tx_in_index
 		self.tx_out_index = tx_out_index
 		self.db_txs = db_txs
+		self.timers = timers
+		self.client_th = client_th
 		
 		self.sock = None
 		self.work = True
@@ -470,6 +472,26 @@ class ServerThread(Thread):
 				resp["confirm_ok"] = tx.out_seeds[2]
 				peer.up_in = True
 			
+			elif "idtysig" in url:
+				doc = utils.verify_idty_sig(self.conf, content, self.conf["idty"]["pubkey"], self.keys.pubkey, utils.self_checktime)
+				
+				if not doc:
+					send_response(client, "403 Forbidden", {"error": "bad_idty_sig"}, resp_format)
+					continue
+				
+				self.conf = read_config(DIR, {
+					"idty.sig": content.hex(),
+					"idty.sigtime": doc["sigtime"]
+				})
+				utils.logprint("Updated idty sig: sigtime= {}".format(doc["sigtime"]), utils.LOG_INFO)
+				
+				self.timers["next_peer_info"] = time.time() + self.conf["server"]["peer_info_interval"]
+				self.local_peer = utils.Peer.generate(self.conf, self.keys, self.peers)
+				utils.logprint("Generated new peer info", utils.LOG_TRACE)
+				self.client_th.spread_peer_info()
+				
+				resp["idtysig_ok"] = doc
+			
 			if "getconfirm" in url:
 				sender_pubkey = utils.getargv("getconfirm", "", 1, url)
 				try:
@@ -539,7 +561,7 @@ class ServerThread(Thread):
 		self.sock.shutdown(socket.SHUT_WR)
 
 class ClientThread(Thread):
-	def __init__(self, conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs):
+	def __init__(self, conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs, timers):
 		Thread.__init__(self)
 		
 		self.conf = conf
@@ -550,6 +572,7 @@ class ClientThread(Thread):
 		self.tx_in_index = tx_in_index
 		self.tx_out_index = tx_out_index
 		self.db_txs = db_txs
+		self.timers = timers
 		
 		self.bma_endpoints = conf["client"]["bma_hosts"].copy()
 		self.work = True
@@ -740,7 +763,7 @@ class ClientThread(Thread):
 		t = time.time()
 		next_mix = t + self.conf["mix"]["mix_interval"]
 		next_peers_detection = t + self.conf["server"]["peer_detect_interval"]
-		next_peer_info = t + self.conf["server"]["peer_info_interval"]
+		self.timers["next_peer_info"] = t + self.conf["server"]["peer_info_interval"]
 		
 		self.detect_peers()
 		local_peer = utils.Peer.generate(self.conf, self.keys, self.peers)
@@ -819,11 +842,11 @@ class ClientThread(Thread):
 						utils.logprint("No peer for: "+tx.receiver_pubkey, utils.LOG_WARN)
 			
 			# Generate peer info
-			if t > next_peer_info:
+			if t > self.timers["next_peer_info"]:
+				self.timers["next_peer_info"] = time.time() + self.conf["server"]["peer_info_interval"]
 				self.local_peer = utils.Peer.generate(self.conf, self.keys, self.peers)
 				utils.logprint("Generated new peer info", utils.LOG_TRACE)
 				self.spread_peer_info()
-				next_peer_info = time.time() + self.conf["server"]["peer_info_interval"]
 			
 			# Remove expired requests
 			expire_txs = []
@@ -850,15 +873,6 @@ def get_credentials(conf):
 		password = getpass.getpass("Enter your password: ")
 	return salt, password
 
-def gen_idty_sig(keys, idty_keys):
-	doc = {
-		"doctype": "gmixer/idtysig",
-		"docver": "1",
-		"pubkey": keys.pubkey,
-		"sigtime": time.time()
-	}
-	return idty_keys.sign(ubjson.dumpb(doc))
-
 # Main function
 def main():
 	# Load conf
@@ -888,9 +902,11 @@ def main():
 	# Generate peer info
 	local_peer = utils.Peer.generate(conf, keys, {})
 	
+	timers = {"next_peer_info": 0}
+	
 	# Start threads
-	clientThread = ClientThread(conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs)
-	serverThread = ServerThread(conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs)
+	clientThread = ClientThread(conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs, timers)
+	serverThread = ServerThread(conf, peers, keys, local_peer, pool, tx_in_index, tx_out_index, db_txs, timers, clientThread)
 	
 	clientThread.start()
 	serverThread.start()
@@ -965,8 +981,10 @@ if __name__ == "__main__":
 				)
 				print(idty_keys.pubkey)
 				loop = input("Is that the right pubkey? [yn]: ").lower() != "y"
-			conf_overwrite["idty.sig"] = gen_idty_sig(keys, idty_keys).hex()
+			sig, doc = utils.gen_idty_sig(keys, idty_keys)
+			conf_overwrite["idty.sig"] = sig.hex()
 			conf_overwrite["idty.pubkey"] = idty_keys.pubkey
+			conf_overwrite["idty.sigtime"] = doc["sigtime"]
 		
 		conf = read_config(DIR, conf_overwrite)
 	
diff --git a/utils.py b/utils.py
index 409de78..db67a92 100644
--- a/utils.py
+++ b/utils.py
@@ -132,19 +132,35 @@ def run_async(coro):
 
 #-------- ÄžMixer
 
-def verify_idty_sig(conf, raw, idty_pubkey, peer_pubkey):
+def gen_idty_sig(keys, idty_keys):
+	doc = {
+		"doctype": "gmixer/idtysig",
+		"docver": "1",
+		"pubkey": keys.pubkey if isinstance(keys, SigningKey) else keys.base58(),
+		"sigtime": int(time.time())
+	}
+	return idty_keys.sign(ubjson.dumpb(doc)), doc
+
+def default_checktime(conf, data):
+	t = time.time()
+	return data["sigtime"] < t and data["sigtime"] + conf["server"]["idty_sig_age_max"] > t
+
+def self_checktime(conf, data):
+	t = time.time()
+	return data["sigtime"] > conf["idty"]["sigtime"] and data["sigtime"] < t and data["sigtime"] + conf["server"]["idty_sig_age_max"] > t
+
+def verify_idty_sig(conf, raw, idty_pubkey, peer_pubkey, checktime=default_checktime):
 	try:
 		raw = libnacl.sign.Verifier(PublicKey(idty_pubkey).hex_pk()).verify(raw)
 		data = ubjson.loadb(raw)
 		assert data["doctype"] == "gmixer/idtysig" , "Bad doctype"
 		assert data["docver"] == "1" , "Bad docver"
 		assert data["pubkey"] == peer_pubkey , "Bad pubkey"
-		t = time.time()
-		assert data["sigtime"] < t and data["sigtime"] + conf["server"]["idty_sig_age_max"] > t , "Bad sigtime"
+		assert checktime(conf, data) , "Bad sigtime"
 	except (ValueError, IndexError, ubjson.decoder.DecoderException, AssertionError) as e:
 		logprint("Bad idty sig: "+str(e), LOG_TRACE)
-		return False
-	return True
+		return None
+	return data
 
 class Peer:
 	VERSION = "2"
-- 
GitLab