m2crypt update

This commit is contained in:
Mario Fetka
2026-04-23 01:55:12 +02:00
parent 577710299a
commit 888447bedf
3 changed files with 206 additions and 111 deletions

View File

@@ -1,8 +1,17 @@
# Copyright 2013 Johannes Fuermann <johannes at fuermann.cc>
# Copyright 2013 Manuel Munz <manu at somakoma.de>
#
# This file is part of fail2ban-p2p.
#
# Licensed under the GNU GENERAL PUBLIC LICENSE Version 3. For details
# see the file COPYING or http://www.gnu.org/licenses/gpl-3.0.en.html.
import json
import M2Crypto
from M2Crypto import EVP
import config
import crypto
import log
import util
import version
@@ -11,7 +20,9 @@ logger = log.initialize_logging("fail2ban-p2p." + __name__)
class Command:
"""Handle command objects."""
"""
Handle command objects.
"""
msgType = None
parameter = ()
@@ -26,9 +37,12 @@ class Command:
self.hops = hops if hops is not None else []
def __string__(self):
return f"Command (msgType = {self.msgType}, ...)"
return "Command (msgType = " + str(self.msgType) + ", ...)"
def toSerializableDict(self):
"""
Returns a recursively sorted dictionary.
"""
unordered_dict = {
"msgType": self.msgType,
"parameter": self.parameter,
@@ -37,6 +51,9 @@ class Command:
return util.sort_recursive(unordered_dict)
def toProtocolMessage(self):
"""
Create a JSON-encoded protocol message.
"""
serializable_dict = self.toSerializableDict()
signed_message = json.dumps(serializable_dict)
signature = self.sign(signed_message)
@@ -48,10 +65,19 @@ class Command:
return json.dumps(signed_dict)
def sign(self, text):
"""
Compute signature for a message.
Args:
text (str): The JSON encoded message text.
Returns:
str: Hex-encoded signature.
"""
logger.debug("signing outgoing message")
c = config.Config()
signer = M2Crypto.EVP.load_key(c.privkey)
signer = EVP.load_key(c.privkey)
signer.sign_init()
signer.sign_update(text.encode("utf-8"))
string_signature = signer.sign_final().hex()

View File

@@ -1,7 +1,14 @@
import os
import sys
# Copyright 2013 Johannes Fuermann <johannes at fuermann.cc>
# Copyright 2013 Manuel Munz <manu at somakoma.de>
#
# This file is part of fail2ban-p2p.
#
# Licensed under the GNU GENERAL PUBLIC LICENSE Version 3. For details
# see the file COPYING or http://www.gnu.org/licenses/gpl-3.0.en.html.
import M2Crypto
import os
from M2Crypto import Rand, RSA
import config
import log
@@ -12,14 +19,16 @@ logger = log.initialize_logging("fail2ban-p2p." + __name__)
def create_keys():
"""Create private/public keypair (RSA 1024 bit)."""
if os.path.isfile(c.privkey) or os.path.isfile(c.pubkey):
print("A keypair for this node already exists.")
ask = input('Do you really want to create a new one? [y/N] ')
ask = input("Do you really want to create a new one? [y/N] ")
if ask != "y":
return
M2Crypto.Rand.rand_seed(os.urandom(1024))
Rand.rand_seed(os.urandom(1024))
logger.info("Generating a 1024 bit private/public key pair...")
keypair = M2Crypto.RSA.gen_key(1024, 65537)
keypair = RSA.gen_key(1024, 65537)
try:
keypair.save_key(c.privkey, None)
os.chmod(c.privkey, 0o400)
@@ -28,4 +37,4 @@ def create_keys():
logger.debug("Public key was saved to %s", c.pubkey)
except IOError as e:
logger.error("Could not save the keypair, check permissions! %s", e)
sys.exit(1)
raise

View File

@@ -1,3 +1,11 @@
# Copyright 2013 Johannes Fuermann <johannes at fuermann.cc>
# Copyright 2013 Manuel Munz <manu at somakoma.de>
#
# This file is part of fail2ban-p2p.
#
# Licensed under the GNU GENERAL PUBLIC LICENSE Version 3. For details
# see the file COPYING or http://www.gnu.org/licenses/gpl-3.0.en.html.
import hashlib
import json
import os
@@ -7,7 +15,7 @@ import threading
from select import select
from time import time
import M2Crypto
from M2Crypto import EVP, RSA
import config
import crypto
@@ -47,22 +55,26 @@ class Node:
logger.debug("running version: %s", version.version)
try:
sockets = []
for a in self.addresses:
for address in self.addresses:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((a, int(self.port)))
s.listen(1)
sockets.append(s)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((address, int(self.port)))
sock.listen(1)
sockets.append(sock)
except Exception as e:
logger.warning("Couldn't bind to address %s (Reason: %s)", a, e)
logger.warning("Couldn't bind to address %s (Reason: %s)", address, e)
while self.running:
readable, _, _ = select(sockets, [], [])
for sock in readable:
client_socket, address = sock.accept()
logger.debug("connection from %s", address[0])
t = threading.Thread(target=server.serve, args=(self, client_socket, address), daemon=True)
t.start()
thread = threading.Thread(
target=server.serve,
args=(self, client_socket, address),
daemon=True,
)
thread.start()
except Exception as e:
print(e)
@@ -73,84 +85,101 @@ class Node:
def processMessages(self):
self.lock.acquire()
logger.debug("begin message handling")
for c in self.messageQueue:
if self.uid not in c.hops:
if c.hops[0] == 'local':
del c.hops[0]
c.hops.append(self.uid)
if c.msgType == 1:
if 'Trustlevel' not in c.parameter:
logger.warning("Incoming Message has no Trustlevel, I won't trust it. Never.")
c.parameter['Trustlevel'] = 0
for command in self.messageQueue:
if self.uid not in command.hops:
if command.hops[0] == "local":
del command.hops[0]
command.hops.append(self.uid)
if command.msgType == 1:
if "Trustlevel" not in command.parameter:
logger.warning("Incoming message has no Trustlevel, I won't trust it.")
command.parameter["Trustlevel"] = 0
if c.sender != "local":
c.parameter['Trustlevel'] = int((float(c.sender.trustLevel) / 100 * float(c.parameter['Trustlevel']) / 100) * 100)
logger.debug("Message now has trust level %s", c.parameter['Trustlevel'])
if command.sender != "local":
command.parameter["Trustlevel"] = int(
(float(command.sender.trustLevel) / 100.0)
* (float(command.parameter["Trustlevel"]) / 100.0)
* 100
)
logger.debug("Message now has trust level %s", command.parameter["Trustlevel"])
relay = True
ipindb = False
if len(self.banList) > 0:
if self.banList:
for ban in self.banList:
if ban['AttackerIP'] == c.parameter['AttackerIP']:
if ban["AttackerIP"] == command.parameter["AttackerIP"]:
ipindb = True
logger.debug("IP already in database.")
if int(ban['Timestamp']) != int(c.parameter['Timestamp']):
if c.hops[0] not in ban['Hops']:
trustold = ban['Trustlevel']
trustnew = int(trustold) + int(c.parameter['Trustlevel'])
if int(ban["Timestamp"]) != int(command.parameter["Timestamp"]):
if command.hops[0] not in ban["Hops"]:
trustold = ban["Trustlevel"]
trustnew = int(trustold) + int(command.parameter["Trustlevel"])
if trustnew > 100:
trustnew = 100
ban['Trustlevel'] = trustnew
ban['Hops'].append(c.hops[0])
ban["Trustlevel"] = trustnew
ban["Hops"].append(command.hops[0])
logger.debug("TrustLevel for this IP is now %s", trustnew)
c.parameter['Trustlevel'] = trustnew
command.parameter["Trustlevel"] = trustnew
else:
relay = False
logger.debug("There is already an entry from %s in our database, do nothing with this message.", c.hops[0])
logger.debug(
"There is already an entry from %s in our database, do nothing with this message.",
command.hops[0],
)
else:
relay = False
logger.debug("Timestamp has not changed, do nothing with this message")
if not ipindb:
self.banList.append({
'AttackerIP': c.parameter['AttackerIP'],
'Timestamp': c.parameter['Timestamp'],
'BanTime': self.banTime,
'Trustlevel': c.parameter['Trustlevel'],
'Hops': [c.hops[0]],
})
logger.debug("Added %s to internal banlist", c.parameter['AttackerIP'])
self.banList.append(
{
"AttackerIP": command.parameter["AttackerIP"],
"Timestamp": command.parameter["Timestamp"],
"BanTime": self.banTime,
"Trustlevel": command.parameter["Trustlevel"],
"Hops": [command.hops[0]],
}
)
logger.debug("Added %s to internal banlist", command.parameter["AttackerIP"])
if relay:
if int(c.parameter['Trustlevel']) >= int(config.Config().threshold):
logger.ban(c.parameter['AttackerIP'])
if int(command.parameter["Trustlevel"]) >= int(config.Config().threshold):
logger.ban(command.parameter["AttackerIP"])
else:
logger.debug("Message's trust level (%s) was below our threshold (%s)", c.parameter['Trustlevel'], config.Config().threshold)
logger.debug(
"Message's trust level (%s) was below our threshold (%s)",
command.parameter["Trustlevel"],
config.Config().threshold,
)
for peer in self.friends:
logger.debug("sending message to all friends")
peer.sendCommand(c)
peer.sendCommand(command)
if c.msgType == 3:
sender_uid = c.hops[0]
if command.msgType == 3:
sender_uid = command.hops[0]
for peer in self.friends:
logger.debug("Comparing senders uid (%s) with one of our friends uid (%s)", sender_uid, peer.uid)
logger.debug(
"Comparing senders uid (%s) with one of our friends uid (%s)",
sender_uid,
peer.uid,
)
if peer.uid == sender_uid:
logger.debug("The message is from our friend %s (uid: %s)", peer.name, peer.uid)
logger.debug("Dumping banlist to %s (uid: %s)", peer.name, peer.uid)
if len(self.banList) > 0:
if self.banList:
for ban in self.banList:
cmd = Command()
cmd.msgType = 1
cmd.hops = [self.uid]
cmd.protocolVersion = version.protocolVersion
cmd.parameter = {
"AttackerIP": ban['AttackerIP'],
"Timestamp": ban['Timestamp'],
"Trustlevel": ban['Trustlevel'],
dump_cmd = Command()
dump_cmd.msgType = 1
dump_cmd.hops = [self.uid]
dump_cmd.protocolVersion = version.protocolVersion
dump_cmd.parameter = {
"AttackerIP": ban["AttackerIP"],
"Timestamp": ban["Timestamp"],
"Trustlevel": ban["Trustlevel"],
}
peer.sendCommand(cmd)
peer.sendCommand(dump_cmd)
else:
logger.debug("I know this message, I won't resend it to prevent loops")
logger.debug("end message handling")
@@ -170,9 +199,12 @@ class Node:
self.configPath = c.configPath
self.configFile = c.configFile
with open(c.pubkey, 'r', encoding='utf-8') as fh:
pubkey_file = fh.read()
pubkey = re.findall("-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----", pubkey_file, re.DOTALL | re.M)[0]
pubkey_file = open(c.pubkey, "r", encoding="utf-8").read()
pubkey = re.findall(
"-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----",
pubkey_file,
re.DOTALL | re.M,
)[0]
logger.debug("our own pubkey is: %s", pubkey)
@@ -186,28 +218,36 @@ class Node:
def getFriends(self):
error = False
friendPath = os.path.join(self.configPath, 'friends')
friends = [f for f in os.listdir(friendPath) if os.path.isfile(os.path.join(friendPath, f))]
if not friends:
friendPath = os.path.join(self.configPath, "friends")
friend_files = [f for f in os.listdir(friendPath) if os.path.isfile(os.path.join(friendPath, f))]
if not friend_files:
logger.warning("No friends found. In order to properly use fail2ban-p2p add at least one friend.")
for file in friends:
with open(os.path.join(friendPath, file), 'r', encoding='utf-8') as f:
friendinfo = f.read()
for file in friend_files:
with open(os.path.join(self.configPath, "friends", file), "r", encoding="utf-8") as handle:
friendinfo = str(handle.read())
pubkey = None
try:
pubkey = re.findall("-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----", friendinfo, re.DOTALL | re.M)[0]
pubkey = re.findall(
"-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----",
friendinfo,
re.DOTALL | re.M,
)[0]
except IndexError:
logger.warning("No pubkey found in config for %s", file)
error = True
if pubkey:
logger.debug("read friend's public key: %s", pubkey)
uid = hashlib.sha224(pubkey.encode("utf-8")).hexdigest()
try:
address = re.search(r"address\s*=\s*(.*)", friendinfo).group(1)
except AttributeError:
logger.warning("address not found in config for %s", file)
error = True
try:
port = re.search(r"port\s*=\s*(.*)", friendinfo).group(1)
if not 0 < int(port) < 65536:
@@ -216,6 +256,7 @@ class Node:
except AttributeError:
logger.warning("port not found in config for %s", file)
error = True
try:
trustlevel = re.search(r"trustlevel\s*=\s*(.*)", friendinfo).group(1)
except AttributeError:
@@ -223,19 +264,32 @@ class Node:
error = True
if not error:
obj = friend.Friend(name=file, uid=uid, address=address, port=int(port), trustLevel=int(trustlevel), publicKey=pubkey)
obj.configpath = os.path.join(friendPath, file)
logger.debug("added friend %s (uid=%s, address=%s, port=%s, trustLevel=%s)", file, uid, address, port, trustlevel)
obj = friend.Friend(
name=file,
uid=uid,
address=address,
port=int(port),
trustLevel=int(trustlevel),
publicKey=pubkey,
)
obj.configpath = os.path.join(self.configPath, "friends", file)
logger.debug(
"added friend %s (uid=%s, address=%s, port=%s, trustLevel=%s)",
file,
uid,
address,
port,
trustlevel,
)
self.friends.append(obj)
else:
logger.error("Could not add friend '%s' due to errors in the config file", file)
error = False
def verifyMessage(self, message):
logger.debug("signature in command class is: %s", message.signature)
logger.debug("attempting to verify command")
if message.msgType is None:
if not message.msgType:
logger.warning("Required parameter 'msgType' is missing in received message.")
raise customexceptions.InvalidMessage
if not validators.isInteger(message.msgType):
@@ -254,8 +308,8 @@ class Node:
logger.warning("Signature is missing in received message")
raise customexceptions.InvalidMessage
for h in message.hops:
if not validators.isAlphaNumeric(h):
for hop in message.hops:
if not validators.isAlphaNumeric(hop):
logger.warning("Invalid characters in hops. Only alphanumeric characters are allowed.")
raise customexceptions.InvalidMessage
@@ -267,29 +321,32 @@ class Node:
if "AttackerIP" not in message.parameter:
logger.warning("Required parameter 'AttackerIP' is missing in received message.")
raise customexceptions.InvalidMessage
if not validators.isIPv4address(message.parameter['AttackerIP']):
if not validators.isIPv4address(message.parameter["AttackerIP"]):
logger.warning('Invalid parameter "AttackerIP" in received message.')
raise customexceptions.InvalidMessage
if "Timestamp" not in message.parameter:
logger.warning("Required parameter 'Timestamp' is missing in received message.")
raise customexceptions.InvalidMessage
if not validators.isInteger(message.parameter['Timestamp']):
if not validators.isInteger(message.parameter["Timestamp"]):
logger.warning('Invalid parameter "Timestamp" in received message.')
raise customexceptions.InvalidMessage
if 'Trustlevel' not in message.parameter:
if "Trustlevel" not in message.parameter:
logger.warning('Required parameter "Trustlevel" in missing in received message.')
raise customexceptions.InvalidMessage
if not (validators.isInteger(message.parameter['Trustlevel']) and 0 <= int(message.parameter['Trustlevel']) <= 100):
if not (
validators.isInteger(message.parameter["Trustlevel"])
and 0 <= int(message.parameter["Trustlevel"]) <= 100
):
logger.warning('Invalid parameter "Trustlevel" in received message.')
raise customexceptions.InvalidMessage
elif message.msgType == 2 or message.msgType == 3:
elif message.msgType in (2, 3):
if "TimeFrame" not in message.parameter:
logger.warning("Required parameter 'TimeFrame' is missing in received message.")
raise customexceptions.InvalidMessage
if not validators.isInteger(message.parameter['TimeFrame']):
if not validators.isInteger(message.parameter["TimeFrame"]):
logger.warning('Invalid parameter "TimeFrame" in received message.')
raise customexceptions.InvalidMessage
else:
@@ -301,32 +358,35 @@ class Node:
logger.debug("Last hop's uid is: %s", last_hop_uid)
sender = None
pk = None
for peer in self.friends:
logger.debug("Comparing last hops uid (%s) with one of our friends uid (%s)", last_hop_uid, peer.uid)
if peer.uid == last_hop_uid:
logger.debug("The message seems to be from our friend %s (uid: %s)", peer.name, peer.uid)
sender = peer
pk = M2Crypto.RSA.load_pub_key(sender.configpath)
pk = RSA.load_pub_key(sender.configpath)
break
if last_hop_uid == "local":
logger.debug("This message was signed with our own key.")
c = config.Config()
pk = M2Crypto.RSA.load_pub_key(c.pubkey)
sender = "local"
break
if sender is None:
if last_hop_uid == "local":
logger.debug("This message was signed with our own key.")
c = config.Config()
pk = RSA.load_pub_key(c.pubkey)
sender = "local"
if sender is None or pk is None:
logger.warning("The message could not be mapped to one of our friends!")
raise customexceptions.InvalidSignature
verifier = M2Crypto.EVP.PKey()
verifier = EVP.PKey()
verifier.assign_rsa(pk)
verifier.verify_init()
verifier.verify_update(json.dumps(message.toSerializableDict()).encode("utf-8"))
if verifier.verify_final(bytes.fromhex(message.signature)) != 1:
logger.warning('Signature doesnt match!')
logger.warning("Signature doesnt match!")
return False
logger.debug('Signature verified successfully')
logger.debug("Signature verified successfully")
message.sender = sender
return True
@@ -338,7 +398,7 @@ class Node:
timeframestart = int(time()) - int(timeframe)
logger.debug("Dumping all nodes that were inserted after %s", timeframestart)
for entry in self.banList:
if int(entry['Timestamp']) > int(timeframestart):
if int(entry["Timestamp"]) > int(timeframestart):
banlist.append(entry)
return json.dumps(banlist)
@@ -346,22 +406,22 @@ class Node:
def requestBanlist(self):
for peer in self.friends:
logger.debug("Sending dump request to %s", peer.name)
c = Command()
c.msgType = 3
c.hops = [self.uid]
c.protocolVersion = version.protocolVersion
c.parameter = {"TimeFrame": self.banTime or 3600}
peer.sendCommand(c)
command = Command()
command.msgType = 3
command.hops = [self.uid]
command.protocolVersion = version.protocolVersion
command.parameter = {"TimeFrame": self.banTime or 3600}
peer.sendCommand(command)
def cleanBanlist(self):
logger.debug("Purging all entries from banlist that are older than %s seconds.", self.banTime)
if len(self.banList) > 0:
if self.banList:
banListKeep = []
for ban in self.banList:
if ban['Timestamp'] + self.banTime > time():
if ban["Timestamp"] + self.banTime > time():
banListKeep.append(ban)
else:
logger.info("Removed %s from internal banlist because the ban has expired.", ban['AttackerIP'])
logger.info("Removed %s from internal banlist because the ban has expired.", ban["AttackerIP"])
self.banList = banListKeep
global cleaner
cleaner = threading.Timer(60, self.cleanBanlist)