diff --git a/README.md b/README.md index 69ca206..fcc0d70 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,28 @@ # jeb -jCloud Event Bus \ No newline at end of file +jCloud Event Bus + +## jebp (jCloud Event Bus Protocol) + +### Messages +All messages begin with `0x01`, followed by the content length length. The content length length is the length of the content length and representated by one byte. Then follows the content length and after that the message content. + +### Handshake +1. Server: Server protocol, version +2. Encryption + 1. Client: Serialized client public key + 2. Server: Serialized server public key + 3. Client: Client nonce + 4. Server: Server nonce + 5. Client: Random bytes (encrypted) + 6. Server: received bytes from client (not encrypted) + + The client closes the connection if the received bytes from server (7.) do not match the generated bytes (6.) to prevent malfunction and test the encryption. +3. Server authentication + 1. Server: Server certificate + + The client closes the connection if it does not trust the server certificate. +4. Client authentication + 1. Client: Client certificate + + The server closes the connection if the client is unauthorized. \ No newline at end of file diff --git a/client/client.py b/client/client.py new file mode 100644 index 0000000..a8214a3 --- /dev/null +++ b/client/client.py @@ -0,0 +1,93 @@ +import sys +import os +sys.path.append(os.getcwd()) +import asyncio +from lib.crypto_utils import ( + generate_keypair, + serialize_public_key, + deserialize_public_key, + derive_aes_key, +) +from lib.jebp_utils import sendmsg, readmsg, MessageFormatError, InvalidCertificateError, validate_cert +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.backends import default_backend +from cryptography import x509 +import base64 +import dbm + +HOST = "127.0.0.1" +PORT = 8888 +KNOWN_PROTOCOLS = (b'jebp 1.0',) +SERVER_CERT_FILE = 'server/sec/server.crt.pem' +CLIENT_CERT_FILE = 'client/sec/client.crt.pem' +REQUIRED_CERT_COMMON_NAME = 'jeb' +REQUIRED_ISSUER_CERT_COMMON_NAME = 'jCloudCA-Root-CA' + +async def main(): + reader, writer = await asyncio.open_connection(HOST, PORT) + + try: + try: + assert await readmsg(reader) in KNOWN_PROTOCOLS + except AssertionError: + print('Unknown protocol') + writer.close() + await writer.wait_closed() + return + + # BEGIN ENCRYPTION HANDSHAKE + + # 1. Client ECC keys + client_priv, client_pub = generate_keypair() + + # 2. Send client public key + await sendmsg(serialize_public_key(client_pub), writer) + + # 3. Receive server public key + server_pub_bytes = await readmsg(reader) + server_pub = deserialize_public_key(server_pub_bytes) + + # 4. Derive shared AES key + aes_key = derive_aes_key(client_priv, server_pub) + aesgcm = AESGCM(aes_key) + + client_nonce = os.urandom(12) + await sendmsg(client_nonce, writer) + + server_nonce = await readmsg(reader) + + test_bytes = os.urandom(32) + await sendmsg(test_bytes, writer, aesgcm, client_nonce) + rec = await readmsg(reader) + if rec != test_bytes: + raise Exception('encryption handshake failed') + await sendmsg(b'', writer) + + + # BEGIN SERVER AUTHENTICATION HANDSHAKE + + cert_data = await readmsg(reader, aesgcm, server_nonce) + cert = x509.load_der_x509_certificate(cert_data, default_backend()) + if not validate_cert(cert, REQUIRED_CERT_COMMON_NAME, REQUIRED_ISSUER_CERT_COMMON_NAME): + raise InvalidCertificateError('certificate not trusted') + + + # BEGIN CLIENT AUTHENTICATION HANDSHAKE + + with open(CLIENT_CERT_FILE, 'rb') as certfile: + cert_data = certfile.read() + certfile.close() + await sendmsg(base64.b64decode(cert_data.replace(b'-----BEGIN CERTIFICATE-----', b'').replace(b'-----END CERTIFICATE-----', b'').strip()), writer, aesgcm, client_nonce) + + + except MessageFormatError: + print('invalid message format') + except Exception as e: + print(f'{str(type(e))[8:-2]}: {e}') + finally: + writer.close() + await writer.wait_closed() + return + + +asyncio.run(main()) \ No newline at end of file diff --git a/client/sec/client.crt.pem b/client/sec/client.crt.pem new file mode 100644 index 0000000..e83a706 --- /dev/null +++ b/client/sec/client.crt.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEXjCCAkYCFDyqhr6D93HlZQSik8KgrPVXMHyiMA0GCSqGSIb3DQEBCwUAMHkx +CzAJBgNVBAYTAkRFMRwwGgYDVQQIDBNOb3JkcmhlaW4tV2VzdGZhbGVuMREwDwYD +VQQHDAhCb3JuaGVpbTERMA8GA1UECgwIakNsb3VkQ0ExCzAJBgNVBAsMAklUMRkw +FwYDVQQDDBBqQ2xvdWRDQS1Sb290LUNBMB4XDTI1MTIyOTIzMzcxNFoXDTI2MTIy +OTIzMzcxNFowXjELMAkGA1UEBhMCREUxHDAaBgNVBAgME05vcmRyaGVpbi1XZXN0 +ZmFsZW4xETAPBgNVBAcMCEJvcm5oZWltMQ8wDQYDVQQKDAZqQ2xvdWQxDTALBgNV +BAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbAEzcpkoL +DCIFUoa9JvOiIQJoe9j4cDsI65M1kczRzxGP5xJEtAJ6fz5gLIo+S4T35kXa/XzY +KeAQ2nYfaaF6BRDlRYtkwQUgpyMyN6h7nxlFbtF2iuogdLie1CXIAanWNFul1QF2 +Z2o74xh2KA0AiVuMk1Weg91TbKsT8loXkC9Xn6mqCwT43gf9JxAXFPLzvWARi9kQ +Srp+nkYr7sCRFbyGaA1KqZMJD0+rwWin4UxMkJtmM5FIPEgHI6iRhcXPtHiGQuFl +bKectfTBiEKb1g9DiY6bitcvseNse6v2XWp05pBp75ZOkygDyaF4Y2eKd/ixgcRv +NRJztk5cu0CDAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAExB5wKUbxvKSmqrwyT7 +ZmPDDXYAARIu4hGF1nDMlUdcbnUNncD1CMP0piF2B/VDoyFRAdwTTzROzmtBoyUl +tEy+d3YOJns5k+qgsMPFmbrEADipvDDOvXJ3/tgpN0tcXoaWBiDENdreH88olKwy +kDbC5fzve74DoQ/sC8ldTgNa/aZp8vILkt5j3UdK7IYSRLFhxyjjGoVxSb/y+Ja8 +98ImQknkv7laSrUNZFesC7DwT98S0G9OZZTt6Ksp4aZSTsr63ugC2Yo91Z9CC4z/ +qPP6v71YDHLtMzMbHahnlmV1AeMOVuIwioVBHg+ewjP0z71ixp/aTeJa7KyOZFii +fXSPy6WshdQFipr7v2g8eHTtwkyqehEhiSreeVfE1LZaCbyDfVXHaZeZUfrk4tRb +Eg50U0AMkdk5+JKRSetmMPpNjU56v8piKSHfoT5K2UGzOn9ymChkkNW8W4c0Q7CR +nJKp6Zzkd8RTJ45HUSbaTR5VVhV5VUHNuatNjsngezZtCqy95WxCJNzVy5m6Hx/c ++Lm5ku/6hVNpugLi5xuPwqjAKZzVgGAuV9le4TjVL+wn75Kx3KsfQvnoLySgsTtR +4Z4vdIbK4pxoKVdUvuyaUAopGr0ZHRX9LzMkWX/iswGB90KlTbUDzvw56mtoVPTI +v4Yoc/RsZTUA4j52LnO4vnW7 +-----END CERTIFICATE----- diff --git a/client/sec/client.csr.pem b/client/sec/client.csr.pem new file mode 100644 index 0000000..266b026 --- /dev/null +++ b/client/sec/client.csr.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICozCCAYsCAQAwXjELMAkGA1UEBhMCREUxHDAaBgNVBAgME05vcmRyaGVpbi1X +ZXN0ZmFsZW4xETAPBgNVBAcMCEJvcm5oZWltMQ8wDQYDVQQKDAZqQ2xvdWQxDTAL +BgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbAEzc +pkoLDCIFUoa9JvOiIQJoe9j4cDsI65M1kczRzxGP5xJEtAJ6fz5gLIo+S4T35kXa +/XzYKeAQ2nYfaaF6BRDlRYtkwQUgpyMyN6h7nxlFbtF2iuogdLie1CXIAanWNFul +1QF2Z2o74xh2KA0AiVuMk1Weg91TbKsT8loXkC9Xn6mqCwT43gf9JxAXFPLzvWAR +i9kQSrp+nkYr7sCRFbyGaA1KqZMJD0+rwWin4UxMkJtmM5FIPEgHI6iRhcXPtHiG +QuFlbKectfTBiEKb1g9DiY6bitcvseNse6v2XWp05pBp75ZOkygDyaF4Y2eKd/ix +gcRvNRJztk5cu0CDAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAmqHeGJsjdPXt +Bk2xQ8VNfgMKebULLmgB9WxP2agycRWS54guDUTD0dgl+ROZ2WKAKCI5fC0EAbd9 +7dzZX5RFRA0h0ZcbVsYhKtKVgL6rq1ujsVfL0YKrsQw/Uduz5Sqdo8IRvMbI748Z +tkiJO4rsvdXay7NMKAByMoKINXIYVj/gMpwyBsbuzxytLGODxCSP3bpCYwpL8DB9 +fGn1s/Dq8gVCIMJ2CBLjVhgBGNAb2eNhYBRysXCn8gP0S4DVW/9emi+Tu3Ckgk3C +siPSls2jmo+qM9kxaTZv684lsUIZlOjzYpRn/nsLpxB6O1VIBSM/fQ1bokKeLIVw +Pjd+o76eAw== +-----END CERTIFICATE REQUEST----- diff --git a/client/sec/client.key.pem b/client/sec/client.key.pem new file mode 100644 index 0000000..97bbdaa --- /dev/null +++ b/client/sec/client.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCbAEzcpkoLDCIF +Uoa9JvOiIQJoe9j4cDsI65M1kczRzxGP5xJEtAJ6fz5gLIo+S4T35kXa/XzYKeAQ +2nYfaaF6BRDlRYtkwQUgpyMyN6h7nxlFbtF2iuogdLie1CXIAanWNFul1QF2Z2o7 +4xh2KA0AiVuMk1Weg91TbKsT8loXkC9Xn6mqCwT43gf9JxAXFPLzvWARi9kQSrp+ +nkYr7sCRFbyGaA1KqZMJD0+rwWin4UxMkJtmM5FIPEgHI6iRhcXPtHiGQuFlbKec +tfTBiEKb1g9DiY6bitcvseNse6v2XWp05pBp75ZOkygDyaF4Y2eKd/ixgcRvNRJz +tk5cu0CDAgMBAAECggEAGHmlKWhWo8TwME/2N7MJYI3+z9nSinRKVh/AuIJDydN9 +LfBqWY/lHkuuYUhXMfwBzJU/bZN+XbHLEE42vcITve1D3DgtiSTTdvL9Y0YXpCGc +v3J+v82yp5pWtrnAF4NvuoO8/wQR/YzZ4Qf8ilfLqcyGuL1hFS00SyKLJxSrNjIO +zzGf8jXPIF7KzzWEWCqDJHCN4VctB0chX0FfhXOLGTDQse/ZdzBOdjWKKJaRLBjG +0g+sSmahZT5zJjAmj/6PjfYhic6ErVfDAwuzLJ9nMr8CoVPGTUaiNR9c93h/VBlC +M6+sp6FDdU6YCIu0rzoHbqtybNhAXIGd5CJrZM2f0QKBgQDSTtlyRT67UChw38s/ +lyNrkl5G3sIX969cW0O/eoH9iy5LNJolngXpOH3/KwSmOShizZmx+r7Qmp+idfPj +DTfe+Gnr6p89ekbpJ1i+l8DcjfUcIFMygrwD7fW3xeAQ/nJTkzozClSocn9wAsob +j55AP2Ocz1ziOmM692QQttoKEwKBgQC8rVUS/vugseBxuSys0r6l0k6zoJLNZFmy +gm138PZm9IpLg4nqUgSIfg3sJoZqkmVGfVtQbS1jjjYDDFY1QRai1gM63Ph3h6QL +/eMjFRJFtKsyiEsvlG/k3SL9x0zCIEnT+rLf7rdDovS55JSsqa7pAr+6b80bxwI+ +c2uYsLO90QKBgQDGaLdC1EszopMUsj3pN2imQweIqv3IaNdbNYr76dMbZaR+NRk8 +ZhJDjhVol6giPgh49mmK/PnqigYS2l8GFWFhjVE4zjf/Yw3lR0a1QSwlqBPXvjNf +kvFYb7aC8z7KKZOof1zH7HYkGSlbfnY4fE0bZfJPbV6+28DkT6NrFWctWwKBgQCa +QyGuoVl8flKiyKLVPo1vqG5+gQfl9Gk+AVOdYB8l+ERmD0sgkSRxsJaTgMAfvEgf +hPi23jzhC/HvNhP6AJiQVGhZpTdlCzq+LzuZgG3rHhdm/nZylWuS9JbaZSvGAH48 +WdoMKvId08tBfbltHmMK0huORECvuFuGUfoj4j1jMQKBgQDGxYkgzgNF1uzrr/lv +OuMZ06GO5PENAsh8WckiNFN7RAt5YS0alXbDazGInT/Qm5sKCZz8GHK7coes8diC +7U+9jlyXC6dMMuCRr8vOxJ4VmOz9suF0ZXQmVJTkjbTvM2UWiJE6Zo7C2UgNo+Cb +JEBKPquHhcULvqN9fYlMVbvguQ== +-----END PRIVATE KEY----- diff --git a/container/Dockerfile b/container/Dockerfile new file mode 100644 index 0000000..2564b5b --- /dev/null +++ b/container/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13-slim + +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8888 + +CMD ["python3", "main.py"] \ No newline at end of file diff --git a/container/requirements.txt b/container/requirements.txt new file mode 100644 index 0000000..488c3aa --- /dev/null +++ b/container/requirements.txt @@ -0,0 +1 @@ +cryptography \ No newline at end of file diff --git a/lib/__pycache__/crypto_utils.cpython-313.pyc b/lib/__pycache__/crypto_utils.cpython-313.pyc new file mode 100644 index 0000000..621c2a7 Binary files /dev/null and b/lib/__pycache__/crypto_utils.cpython-313.pyc differ diff --git a/lib/__pycache__/jebp_utils.cpython-313.pyc b/lib/__pycache__/jebp_utils.cpython-313.pyc new file mode 100644 index 0000000..f1cb593 Binary files /dev/null and b/lib/__pycache__/jebp_utils.cpython-313.pyc differ diff --git a/lib/__pycache__/terminal_table.cpython-313.pyc b/lib/__pycache__/terminal_table.cpython-313.pyc new file mode 100644 index 0000000..5b4fa01 Binary files /dev/null and b/lib/__pycache__/terminal_table.cpython-313.pyc differ diff --git a/lib/crypto_utils.py b/lib/crypto_utils.py new file mode 100644 index 0000000..67749c1 --- /dev/null +++ b/lib/crypto_utils.py @@ -0,0 +1,32 @@ +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + + +def generate_keypair(): + private_key = ec.generate_private_key(ec.SECP256R1()) + return private_key, private_key.public_key() + + +def serialize_public_key(public_key: ec.EllipticCurvePublicKey) -> bytes: + return public_key.public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint, + ) + + +def deserialize_public_key(data: bytes) -> ec.EllipticCurvePublicKey: + return ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), data + ) + + +def derive_aes_key(private_key, peer_public_key) -> bytes: + shared_secret = private_key.exchange(ec.ECDH(), peer_public_key) + + return HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=b'handshake', + ).derive(shared_secret) diff --git a/lib/jebp_utils.py b/lib/jebp_utils.py new file mode 100644 index 0000000..ae5146d --- /dev/null +++ b/lib/jebp_utils.py @@ -0,0 +1,47 @@ +import asyncio +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography import x509 +from datetime import datetime, timezone + +MSG_START_BYTE = b'\x01' + +class MessageFormatError(Exception): ... +class InvalidCertificateError(Exception): ... + +def int_to_bytes(n: int, signed = False): + n = int(n) + return n.to_bytes((n.bit_length() + 7) // 8, signed = signed) + +async def sendmsg(m, writer: asyncio.StreamWriter, aesgcm: AESGCM = None, aesnonce = None, start_byte = MSG_START_BYTE): + if type(m) == str: + m = m.encode() + if aesgcm: + m = aesgcm.encrypt(aesnonce, m, None) + content_length = int_to_bytes(len(m)) + writer.write(start_byte + bytes([len(content_length)]) + content_length + m) + await writer.drain() + +async def readmsg(reader: asyncio.StreamReader, aesgcm: AESGCM = None, aesnonce = None, start_byte = MSG_START_BYTE): + if await reader.readexactly(1) != start_byte: + raise MessageFormatError('invalid message format') + content_length_length = await reader.readexactly(1) + content_length = await reader.readexactly(int.from_bytes(content_length_length)) + m = await reader.readexactly(int.from_bytes(content_length)) + if aesgcm: + return aesgcm.decrypt(aesnonce, m, None) + return m + +def validate_cert(cert: x509.Certificate, common_name, issuer_common_name, now = datetime.now(timezone.utc)): + if not cert.not_valid_before_utc <= now <= cert.not_valid_after_utc: + return False + cn = [a.value for a in cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)] + if len(cn) < 1: + return False + if common_name not in cn: + return False + cn = [a.value for a in cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)] + if len(cn) < 1: + return False + if issuer_common_name not in cn: + return False + return True \ No newline at end of file diff --git a/lib/terminal_table.py b/lib/terminal_table.py new file mode 100644 index 0000000..635c5ef --- /dev/null +++ b/lib/terminal_table.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +class TableBorderCharset: + def __init__(self, + corner_top_left, corner_top_right, corner_bottom_left, corner_bottom_right, + line_horizontal, line_vertical, + t_junction_horizontal_top, t_junction_horizontal_bottom, t_junction_vertical_left, t_junction_vertical_right, + intersection + ): + self.corner_top_left = corner_top_left + self.corner_top_right = corner_top_right + self.corner_bottom_left = corner_bottom_left + self.corner_bottom_right = corner_bottom_right + self.line_horizontal = line_horizontal + self.line_vertical = line_vertical + self.t_junction_horizontal_top = t_junction_horizontal_top + self.t_junction_horizontal_bottom = t_junction_horizontal_bottom + self.t_junction_vertical_left = t_junction_vertical_left + self.t_junction_vertical_right = t_junction_vertical_right + self.intersection = intersection + + + +BORDER_THIN = TableBorderCharset( + '┌', '┐', '└', '┘', + '─', '│', + '┴', '┬', '┤', '├', + '┼' +) + +BORDER_THICK = TableBorderCharset( + '┏', '┓', '┗', '┛', + '━', '┃', + '┻', '┳', '┫', '┣', + '╋' +) + +BORDER_DOUBLE = TableBorderCharset( + '╔', '╗', '╚', '╝', + '═', '║', + '╩', '╦', '╣', '╠', + '╬' +) + +BORDER_ROUND = TableBorderCharset( + '╭', '╮', '╰', '╯', + '─', '│', + '┴', '┬', '┤', '├', + '┼' +) + +def get_max_widths(data): + max_widths = {} + for row in data: + for col in row: + max_widths[col] = max(len(str(row[col])), max_widths.get(col, 0)) + return max_widths + + +def ascii_table(data, *, borders = False, border_charset = BORDER_THIN, column_space = 1, uppercase_column_headers = True): + column_widths = get_max_widths(data) + columns = list(column_widths.keys()) + table = f'''{(column_space * ' ').join([(c.upper() if uppercase_column_headers else c) + ' ' * (column_widths[c] - len(c)) for c in columns])}''' + table = '' + for col in columns: + column_widths[col] = max(column_widths[col], len(col)) + for col in columns: + table += col.upper() if uppercase_column_headers else col + table += ' ' * (column_widths[col] - len(col)) + table += ' ' * column_space + if borders: + table += border_charset.line_vertical + ' ' + for row in data: + table += '\n' + for column in columns: + value = str(row.get(column, '')) + table += value + table += ' ' * (column_widths[column] - len(str(value))) + table += column_space * ' ' + if borders: + table += border_charset.line_vertical + ' ' + if borders: + table_without_borders = table + table_width = max([len(r) for r in table_without_borders.split('\n')]) + table = border_charset.corner_top_left + for col in columns: + table += border_charset.line_horizontal * (column_widths[col] + 2) + table += border_charset.t_junction_horizontal_bottom + table = table[:-1] + border_charset.corner_top_right + table += '\n' + for row in table_without_borders.split('\n'): + table += border_charset.line_vertical + ' ' + table += row + table += '\n' + table += border_charset.t_junction_vertical_right + for i in range(len(columns)): + table += border_charset.line_horizontal * (column_widths[columns[i]] + 2) + table += border_charset.intersection if i != (len(columns) - 1) else border_charset.t_junction_vertical_left + table += '\n' + table = '\n'.join(table.split('\n')[:-2]) + '\n' + table += border_charset.corner_bottom_left + for col in columns: + table += border_charset.line_horizontal * (column_widths[col] + 2) + table += border_charset.t_junction_horizontal_top + table = table[:-1] + border_charset.corner_bottom_right + return table \ No newline at end of file diff --git a/server/clients_management/chclient.py b/server/clients_management/chclient.py new file mode 100755 index 0000000..7625b64 --- /dev/null +++ b/server/clients_management/chclient.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +import dbm +import sys +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +import hashlib + +if len(sys.argv) < 2: + print(f'{sys.argv[0]}: missing key hash') + sys.exit(1) + +with dbm.open('server/config/clients/fingerprints', 'c') as db: + if bytes.fromhex(sys.argv[1]) not in db.keys(): + print(f'{sys.argv[0]}: hash not registered') + sys.exit(1) + db[bytes.fromhex(sys.argv[1])] = input('New common name: ') + db.close() \ No newline at end of file diff --git a/server/clients_management/lsclients.py b/server/clients_management/lsclients.py new file mode 100755 index 0000000..0809af8 --- /dev/null +++ b/server/clients_management/lsclients.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +import sys +import os +sys.path.append(os.getcwd()) +import dbm +from lib.terminal_table import ascii_table + +with dbm.open('server/config/clients/fingerprints', 'c') as db: + print(ascii_table([{ + 'Key Hash': k.hex(), + 'Common name': v.decode() + } for k, v in db.items()], )) + db.close() \ No newline at end of file diff --git a/server/clients_management/mkclient.py b/server/clients_management/mkclient.py new file mode 100755 index 0000000..cd61807 --- /dev/null +++ b/server/clients_management/mkclient.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import dbm +import sys +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +import hashlib + +if len(sys.argv) < 2: + print(f'{sys.argv[0]}: missing common name') + sys.exit(1) + +try: + cert = x509.load_pem_x509_certificate(sys.stdin.buffer.read(), default_backend()) +except: + print(f'{sys.argv[0]}: invalid certificate') + sys.exit(1) + +with dbm.open('server/config/clients/fingerprints', 'c') as db: + db[hashlib.sha256(cert.public_key().public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo)).digest()] = sys.argv[1] + db.close() \ No newline at end of file diff --git a/server/clients_management/rmclient.py b/server/clients_management/rmclient.py new file mode 100755 index 0000000..2f08a2d --- /dev/null +++ b/server/clients_management/rmclient.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +import sys +import os +sys.path.append(os.getcwd()) +import dbm +from lib.terminal_table import ascii_table + +if len(sys.argv) < 2: + print(f'{sys.argv[0]}: missing key hash') + sys.exit(1) + +with dbm.open('server/config/clients/fingerprints', 'c') as db: + try: + del db[bytes.fromhex(sys.argv[1])] + except: + print(f'{sys.argv[0]}: hash not registered') + sys.exit(1) + db.close() \ No newline at end of file diff --git a/server/config/clients/fingerprints b/server/config/clients/fingerprints new file mode 100644 index 0000000..9a3a8d2 Binary files /dev/null and b/server/config/clients/fingerprints differ diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..524b7bc --- /dev/null +++ b/server/main.py @@ -0,0 +1,91 @@ +import sys +import os +sys.path.append(os.getcwd()) +import asyncio +from lib.crypto_utils import ( + generate_keypair, + serialize_public_key, + deserialize_public_key, + derive_aes_key, +) +from lib.jebp_utils import sendmsg, readmsg, validate_cert, InvalidCertificateError +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend +from cryptography import x509 +import base64 +import dbm +import hashlib + + +HOST = "0.0.0.0" +PORT = 8888 +VERSION = 'jebp 1.0' +CERT_FILE = 'server/sec/server.crt.pem' +CLIENT_CERT_ISSUER_NAME = 'jCloudCA-Root-CA' + +async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + addr = writer.get_extra_info('peername') + print(f'Connected to {addr}') + + + await sendmsg(VERSION, writer) + + # BEGIN ENCRYPTION HANDSHAKE + + server_priv, server_pub = generate_keypair() + + client_pub_bytes = await readmsg(reader) + client_pub = deserialize_public_key(client_pub_bytes) + + await sendmsg(serialize_public_key(server_pub), writer) + + aes_key = derive_aes_key(server_priv, client_pub) + aesgcm = AESGCM(aes_key) + + client_nonce = await readmsg(reader) + + server_nonce = os.urandom(12) + await sendmsg(server_nonce, writer) + + await sendmsg(await readmsg(reader, aesgcm, client_nonce), writer) + + + # BEGIN SERVER AUTHENTICATION HANDSHAKE + + + await readmsg(reader) + with open(CERT_FILE, 'rb') as certfile: + cert_data = certfile.read() + certfile.close() + + await sendmsg(base64.b64decode(cert_data.replace(b'-----BEGIN CERTIFICATE-----', b'').replace(b'-----END CERTIFICATE-----', b'').strip()), writer, aesgcm, server_nonce) + + + # BEGIN CLIENT AUTHENTICATION HANDSHAKE + + cert = x509.load_der_x509_certificate(await readmsg(reader, aesgcm, client_nonce)) + key_hash = hashlib.sha256(cert.public_key().public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo)).digest() + with dbm.open('server/config/clients/fingerprints', 'c') as db: + if key_hash not in db.keys(): + raise InvalidCertificateError('client not known') + if not validate_cert(cert, db[key_hash].decode(), CLIENT_CERT_ISSUER_NAME): + raise InvalidCertificateError('client certificate not trusted') + + print('Client authenticated') + + writer.close() + await writer.wait_closed() + + +async def main(): + server = await asyncio.start_server(handle_client, HOST, PORT) + addr = ', '.join(str(sock.getsockname()) for sock in server.sockets) + print(f'Listening on {addr}') + + async with server: + await server.serve_forever() + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/server/sec/ca/certs/ca.cert.pem b/server/sec/ca/certs/ca.cert.pem new file mode 100644 index 0000000..13919a8 --- /dev/null +++ b/server/sec/ca/certs/ca.cert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzTCCArWgAwIBAgIUOKOEzdpgmyw7M7cqcJvOqcSBiMEwDQYJKoZIhvcNAQEL +BQAwdjELMAkGA1UEBhMCREUxHDAaBgNVBAgME05vcmRyaGVpbi1XZXN0ZmFsZW4x +ETAPBgNVBAcMCEJvcm5oZWltMQ8wDQYDVQQKDAZqQ2xvdWQxETAPBgNVBAsMCFNl +Y3VyaXR5MRIwEAYDVQQDDAlqQ2xvdWQgQ0EwHhcNMjUxMjI5MTMxOTU1WhcNMzUx +MjI3MTMxOTU1WjB2MQswCQYDVQQGEwJERTEcMBoGA1UECAwTTm9yZHJoZWluLVdl +c3RmYWxlbjERMA8GA1UEBwwIQm9ybmhlaW0xDzANBgNVBAoMBmpDbG91ZDERMA8G +A1UECwwIU2VjdXJpdHkxEjAQBgNVBAMMCWpDbG91ZCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANm+Vys2wsAhkFkOCa85/92GEG9x3Y3CWIpBgbB6 +SvLYk0B3Ed78NvSXpigwYgeATtsctM/pMbEGlh4vhCuRSeNaSkYozet6ZR3/qRhI +hWkULrJE5uLjwFoQkUA94IfF1hfQW5y4vrUnrfDEBoiDUP1oLN5xeNs9MpV4NJR9 +og57k6jrMIKA7Ep1pnszSo5AKfo/xfaI0EmYk8M4xUI2vZUsnh4uZtTfrkoCVsZW +tk6rg82fWW/y7dEHP2oF8y61STY8jO3QCnyPFoDJNaGJagJx9pSCWypCN0+vwTU/ +arfsjnjQQfvKZaCGS9+J27Yn7iIv4gPjZt8L9pMwMSrgC4kCAwEAAaNTMFEwHQYD +VR0OBBYEFIjS2hku4PJxxOyeaOgiVyN3rEwoMB8GA1UdIwQYMBaAFIjS2hku4PJx +xOyeaOgiVyN3rEwoMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB +ACG9lGtheCu4k/J4QjpxcgeVtK7UDy6CM4JVIdNzQP5Q+7uzuNjjo6IpJIjRwjNq +VRsWMtDR2Hlt0cffj0J9V8LVpDz5nah2jvxjRFo1RiBMuEhC2IYMWKdOoIeivwim +XZeUD6SEiR3x2xsJ/ZBHnZeKF33zwecIMwtrTqwMpnH+gmVDgslkR0gu1Y+SEX7n +zbysPRq3yNLW7Tdc9SvfK+2k2JOfur4XjPlXl0LwkAd1yE/MxS4qpLZUAmU/YqQH +zSfuHpTTbe0DqjotyECRs1PaPJMZ3pRFJ6PL6VZE+Xan/0p05gmwv2nimVvtRooS +NgSIHDq1B1qxB3r+VqBEA3g= +-----END CERTIFICATE----- diff --git a/server/sec/ca/private/ca.key.pem b/server/sec/ca/private/ca.key.pem new file mode 100644 index 0000000..e970e15 --- /dev/null +++ b/server/sec/ca/private/ca.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZvlcrNsLAIZBZ +DgmvOf/dhhBvcd2NwliKQYGwekry2JNAdxHe/Db0l6YoMGIHgE7bHLTP6TGxBpYe +L4QrkUnjWkpGKM3remUd/6kYSIVpFC6yRObi48BaEJFAPeCHxdYX0FucuL61J63w +xAaIg1D9aCzecXjbPTKVeDSUfaIOe5Oo6zCCgOxKdaZ7M0qOQCn6P8X2iNBJmJPD +OMVCNr2VLJ4eLmbU365KAlbGVrZOq4PNn1lv8u3RBz9qBfMutUk2PIzt0Ap8jxaA +yTWhiWoCcfaUglsqQjdPr8E1P2q37I540EH7ymWghkvfidu2J+4iL+ID42bfC/aT +MDEq4AuJAgMBAAECggEAZdF1Jm5xa/Fl89a2HZuT80zs44gNr2upBmFyWQkegedX +GAZ1s10h/4boOhPByzsq5JfMTwp+44YubUgP2GWUdP00DRGolMvDe98gfYvv4n5/ +BXplssQIHrVvjmhA1Yxju/gA1sym1MQMuLCZU32AQjbUAGJ0PqSjwjQW9ja3MGio +/if+UMeVhg+Wf1ZYq3Le8GjkYVToZxw9RTCtF4ERBm3pCa3Jpul81kVwIijpBGCk +p72P6nwgHYKWdiwnJksk7TTsodVZNePwX1cPpX0ZpU7sPJeq9i9M+f5d6Lrd1ddb +vS2XFDPb5n5URWdANTtRznEqnscos9jCv2nwC1EyrwKBgQDw9jQ0I3qnP7p6m8++ +8DByLJ1B6LamuCvNaSVKOIhCW2QZF66HJtLXFX65A8W+gEnPtuoBP6rAYLGFYI+z +usiuxEUwkU4ERbMV5BSqjHJIfm3C6T+6SM2ucX+A5WzIxOxnPsER7L/aSXjM114v +38XWTYuh1tmLZ4pwVF8gwcQ3fwKBgQDnVS8U24Kzx7SFfIRcxsBcG5NDkQp69x7O +vDszGF2o/JokCQcmKE/p6j6FMxwUMYZhMEoYeJ+rAkU6hXlk/GgtF/7BS7AoGC8K +AN+sSZlWVQB3p5GbZdUXq4zhflmYzOCa768+nMkTitwaeCzk5IJFU4dZ1Z64/uSW +vxa0RLGA9wKBgHHuR1Kbr+Oiaz6Lq1eBY7aoR5ahPLNpgzR1Ua9dOXf+lD6VBu1a +Ovh9blR2ZyoiqEOxHcWHX+nkDiudM3TsjPStUnWbToMQnZNtKyRUkbibJrSw6nPe +9nlTqtC8KYpoKcrLTih2g2Pnt1lLP3ptWLJyKeJUnrmeNEPWcMAhPfBrAoGATTF6 +tX4OXkl8rrJ/RKo51EKLYJeMpL3DtP7QzlsKHfK0yOOOVChNimPVGwOUEhT4n4PK +peHSffETAtU5Tf6AENQZ+Yx1KtvvDmp+OPKSTWGt9z+ecfmIIw7mwvQzl65+IJVs +p/sPigrjKZ4CdRrfcZzO5KZJAbhktPzs6Zimlr8CgYEAuhcaEE3LeInshaPY4T/p +gcek6bRaScpi7aY0OBH6YPmO55kU+8G+eGv0ykQpu1wHJHaJIUG+s5JqfboUYVsZ +nbfuEeQ0rdcbvvaygbL0+bnDecam0Sa5x9G4UmdW/DeXb4HufCamnrxh2e59CYjZ +tVtU2HF/tg2GyPcQ9sEBIwY= +-----END PRIVATE KEY----- diff --git a/server/sec/server.crt.pem b/server/sec/server.crt.pem new file mode 100644 index 0000000..93633e1 --- /dev/null +++ b/server/sec/server.crt.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIETTCCAjUCFDyqhr6D93HlZQSik8KgrPVXMHygMA0GCSqGSIb3DQEBCwUAMHkx +CzAJBgNVBAYTAkRFMRwwGgYDVQQIDBNOb3JkcmhlaW4tV2VzdGZhbGVuMREwDwYD +VQQHDAhCb3JuaGVpbTERMA8GA1UECgwIakNsb3VkQ0ExCzAJBgNVBAsMAklUMRkw +FwYDVQQDDBBqQ2xvdWRDQS1Sb290LUNBMB4XDTI1MTIyOTE3MDkxMVoXDTI3MTIy +OTE3MDkxMVowTTELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzERMA8GA1UEBwwI +Qm9ybmhlaW0xDzANBgNVBAoMBmpDbG91ZDEMMAoGA1UEAwwDamViMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7zhAY0ytYvzhwuCI0nC7A485Jo74H5tO +x7sZ8XLkNWr/efjZNkEb0zfzN0BDXgWa74qmjv6JcAU7Fdzc3DMfFiCLbmgrQ0ev +FoD5PHRoC2x65LMclJQG9mNk3gWhnyB9mQRlJysi7RNDKTUruUpGg/p2wiEmXRJ3 +xfWZ3Jdp0nvxEYbXrVemHBTcXqEW1Klhy7Ycr4cX7IoqLTk4MIOJrD90KFCyKXMt +6EoHQhhX2IQnX0R1ok/Suxwx+NG7yeDTe5CasqgxTvzuZeT5x8Y3vbv5uIrE6FaA +0XzvvHTtshqCcQeg1guSdzoRlgyMvh8G9l93VDXYFN922Mec9HfJGwIDAQABMA0G +CSqGSIb3DQEBCwUAA4ICAQBOVnuexCELMTymgGtx0GG4ZV/xuyGNOI9fxX0n31C9 +6gQ4Uwe5gqy0O9g1Qw/UrA3Yf4o6R1gzxGZdJGUEKm7R1M3GE2/tChTrlRFbmiA5 +LXC0FOhdhU+S6XBpOoj87i2FLts+4si0+EqxOUvR03EkwZeKJaKttNnHkmuxSMkm +7EJdgqTu7ohcBHlW3Gyc0TGnVX1vMBSkXrLwrRON3Zopd6nlbN/bzEY53aTGlHiN +IWfk+dE4l8ZLnJcihvm6hf2wwVWNGxiJojk5LsNDyqwvY+1dA4xU0QuJYx1rzJHW +cNfiC/qA6u9ml4N7UXZZRL/BTr8RF9zXmtr5hzV4xHWY/GmJBybLLPVlB/3fAkCC +OpNlTlOzqdd0o4y5gjup2ekEi6DFX0Ef0TVLdyJ0+OPm7HrlC76f+kE67ouKQjLT +bbnQ9j6u3GzNfuywWKPSizV/0X8X/zD/ix/jKOpE+JqRApRHLkRf7QGGOVudFYZW +L+C0/7EQNaSPjA5iDFlWOy9j20ZF44uYsL8FYJ5tjs49E9QQ2SwKf55Bdr8Kpxnw +W4zV4S9l+ZXRVK20GuXnpBvMxRDj6ZMeByj0YPyej4gCxnz1PlzT52SwwnwIj3lk +csnIxvJv15ks0ws9IQqXAEmcgu4jU08yQBh0S4LMj4l8IoDJL2bt/OO5KT7LjyLj +aQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/server/sec/server.csr.pem b/server/sec/server.csr.pem new file mode 100644 index 0000000..2a0dfbf --- /dev/null +++ b/server/sec/server.csr.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICkjCCAXoCAQAwTTELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzERMA8GA1UE +BwwIQm9ybmhlaW0xDzANBgNVBAoMBmpDbG91ZDEMMAoGA1UEAwwDamViMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7zhAY0ytYvzhwuCI0nC7A485Jo74 +H5tOx7sZ8XLkNWr/efjZNkEb0zfzN0BDXgWa74qmjv6JcAU7Fdzc3DMfFiCLbmgr +Q0evFoD5PHRoC2x65LMclJQG9mNk3gWhnyB9mQRlJysi7RNDKTUruUpGg/p2wiEm +XRJ3xfWZ3Jdp0nvxEYbXrVemHBTcXqEW1Klhy7Ycr4cX7IoqLTk4MIOJrD90KFCy +KXMt6EoHQhhX2IQnX0R1ok/Suxwx+NG7yeDTe5CasqgxTvzuZeT5x8Y3vbv5uIrE +6FaA0XzvvHTtshqCcQeg1guSdzoRlgyMvh8G9l93VDXYFN922Mec9HfJGwIDAQAB +oAAwDQYJKoZIhvcNAQELBQADggEBAHeuAv3bYqIf2GGHvm0ztgar+npTd4/vPZDm +pZBdo3EAEe+Z9wZJCzSfUfpssfrRk94RPdbO10mdwBGDzcQ+V7Urb8jdFoGt1LRD +P17SvWqfYmGAyML3aA+v1efBkgOIRYPROsoTncP3LrSjRMKKC0MkO2u/QbZMiNce +ZqnzMvb3RtQAnOxNsS12FGEBiBownQrZqQyq/uIsnAjnA6QhoZJOFiXvupz9MyJq +MxeRMw9pBz7p31vhnSaWbrb1hmvhmontdmuQ2aNBEZSSMp+rxKLn3lvAlxyy22Uq +bgahiF6qi5XItOexSkVCBhSC7UACXGZr/9wQacH9FgZEUDCNIYY= +-----END CERTIFICATE REQUEST----- diff --git a/server/sec/server.key.pem b/server/sec/server.key.pem new file mode 100644 index 0000000..259ec88 --- /dev/null +++ b/server/sec/server.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDvOEBjTK1i/OHC +4IjScLsDjzkmjvgfm07HuxnxcuQ1av95+Nk2QRvTN/M3QENeBZrviqaO/olwBTsV +3NzcMx8WIItuaCtDR68WgPk8dGgLbHrksxyUlAb2Y2TeBaGfIH2ZBGUnKyLtE0Mp +NSu5SkaD+nbCISZdEnfF9Zncl2nSe/ERhtetV6YcFNxeoRbUqWHLthyvhxfsiiot +OTgwg4msP3QoULIpcy3oSgdCGFfYhCdfRHWiT9K7HDH40bvJ4NN7kJqyqDFO/O5l +5PnHxje9u/m4isToVoDRfO+8dO2yGoJxB6DWC5J3OhGWDIy+Hwb2X3dUNdgU33bY +x5z0d8kbAgMBAAECggEAcPQtqvWMtT3U1/CvijZCh8a75JIDZOEvjK2y7Ugjq3lv +UkeHQM/zdINpq0ADz0R0SQE97i0P9j3yDTuxaaQV3JvXWnWDYAxcBxM1HC+W5TX+ +vTg5mYpf0z1RZmhgTUPJKlRh8uGyZyD+SFnb4GzK6Qx9wOJoO/A5b9atJPS1ufV0 +RByegK6eQefif6oAThazxb68vMy1By5cFiAEscql0Y45tyzGB/NWYbq9AlIVkHSd +XW3z0Ro1cP76t5SrEDQNZ3udGkwZViAVZS++sOu6aXmKda9c3QzzAzu+//nucFYl +H7xvOZhoi2jyWZWwEwD5JncEtUv+MthqUYMRY3/gmQKBgQD5o3SLasvjHlxD1Vkl +W8fR2pUjaPCjQIuis9IZ8JSGKPzvA0WTnS6Qc0SnGvqrSZxsnNONPS3lnqlDg7MQ +VbiFPbH97MZe3/wuy/9oo+ZxMUi8v1X0WzA4w1iBWUh9egAOT9y4V6VXYfqeZEOE +dIGebeVixSD0aPqfthfDy5un7wKBgQD1UNQKWTh0pvSUAauMHSLmyzGuQQqSyRpS +yhYDgWr3qhu3/FRIIQaCQ5lAxhepJTgs1xysNdyjM2Tg4rSkFXM8L6YJ3cnmd+SE +fXyrasjbehwgcnpXruVGE5iWOzEmf5/n26FunHcsFgV8H+hW2Rrt609Xhi6+7v0x +qAxwCH6llQKBgQDQ344dNO+Bih6i9zkOxBuOH/kXVq+5uMDdEt5hq+Vp15PCnJcz +qpmSbY/szesdNIs4aYOssprbfISdo+1IZqDsgzGRh/J24ayMr012DWU8IoN8wg82 +VLIlXHWKTN0Vd+XiE7pXV6ZVVfqvWq8PfbgSilsa6FvphIjm5yI9RfhoawKBgQCV +cSv7MecGiT6te2b58DX2ywn00YKTcRcsIBfRAuIbBfHk3z6owhWo/W063HJFhHr4 +NgMtlZJXiVOG/BR3cOnGXHCVyhrY32jCnX95HZBGw9imm5aUG+NoET//JQgIH+9V +26I4M/EVVkT5HKb5YUDad2LaOs/3WLe9rFYHWeVtEQKBgG5xsN8GhCGDuWKqHkLe +hUNAGFgutDQzOM9Huk1YRk6DNog7GXRzJYUWM/XP/TqRSlGZu0I3/1VedkKwiKH3 +VDY/IikRvsGC0pfjM/hnh7EVDEjlTClYh+6uZbY2tpcq+FuR1mr/2uRWiLUZIoWs +Cjt9QMUxvxXAnjvXacDAkgCj +-----END PRIVATE KEY-----