diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py index 3c26626496..73487b73ee 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/client/client.py @@ -20,9 +20,9 @@ from cryptography.x509.oid import NameOID from oslo_log import log as logging from sysinv.ipsec_auth.client import config -from sysinv.ipsec_auth.common.constants import State from sysinv.ipsec_auth.common import constants from sysinv.ipsec_auth.common import utils +from sysinv.ipsec_auth.common.objects import State LOG = logging.getLogger(__name__) @@ -235,13 +235,13 @@ class Client(object): if not self._handle_rcvd_data(self.data): raise ConnectionAbortedError("Error receiving data from server") sel.modify(sock, selectors.EVENT_WRITE) - self.state = utils.get_next_state(self.state) + self.state = State.get_next_state(self.state) if mask & selectors.EVENT_WRITE: msg = self._handle_send_data(self.data) sock.sendall(bytes(msg, 'utf-8')) sel.modify(sock, selectors.EVENT_READ) - self.state = utils.get_next_state(self.state) + self.state = State.get_next_state(self.state) if self.state == State.STAGE_5: keep_running = False diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py index 7615845459..dd620dabc7 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/constants.py @@ -3,17 +3,6 @@ # # SPDX-License-Identifier: Apache-2.0 # -from enum import Enum - - -class State(Enum): - STAGE_1 = 1 - STAGE_2 = 2 - STAGE_3 = 3 - STAGE_4 = 4 - STAGE_5 = 5 - - PROCESS_ID = '/var/run/ipsec-server.pid' DEFAULT_BIND_ADDR = "0.0.0.0" diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/objects.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/objects.py new file mode 100644 index 0000000000..2c6342fb5e --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/objects.py @@ -0,0 +1,122 @@ +# +# Copyright (c) 2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +import binascii +import enum +import random +import secrets +import string +import time +import threading + + +class State(enum.Enum): + STAGE_1 = 1 + STAGE_2 = 2 + STAGE_3 = 3 + STAGE_4 = 4 + STAGE_5 = 5 + + @staticmethod + def get_next_state(state): + '''Get the next IPsec Auth state whenever a Stage is finished. + + The IPsec Auth server-client interaction is separated into 5 work stages. + STAGE_1: represents the initial stage where IPsec Auth client send + the first message with OP code, mac address and a hash to + IPsec Auth server. + STAGE_2: represents the stage of validation of the message 1 received + from the client and generation of a response message. If the + validation is satisfied, the IPsec Auth server will encapsulate + an OTS Token, client's hostname, generated public key, + system-local-ca's certificate and a signed hash of this payload + in the response message to send it to the client. + STAGE_3: represents the stage of validation of the message 2 received + from the server and generation of a response message. if the + validation is satisfied, the IPsec Auth Client will encapsulate + an OTS Token, an encrypted Initial Vector (eiv), an encrypted + symetric key (eak1), an encrypted certificate request (eCSR) + and a signed hash of this payload in the response message to + send it to the server. + STAGE_4: represents the stage of validation of the message 3 from the + client and generation of a final response message. If the + validation of the message is satisfied, the IPsec Auth server + will create a CertificateRequest resource with a CSR received + from client's message and will encapsulate the signed + Certificate, network info and a signed hash of this payload in + the response message to send it to the client. + STAGE_5: represents the final stage of IPsec PKI Auth procedure and demands + that IPsec Auth server and client close the connection that + finished STAGE_4. + ''' + if state == State.STAGE_1: + state = State.STAGE_2 + elif state == State.STAGE_2: + state = State.STAGE_3 + elif state == State.STAGE_3: + state = State.STAGE_4 + elif state == State.STAGE_4: + state = State.STAGE_5 + return state + + +class Token(object): + VERSION = int(1).to_bytes(1, 'little') + EXPIRY_TIME = 5000 + + def __init__(self): + self.__nonce = secrets.token_bytes(16) # 128-bit nonce + self.__start_time = int(time.time() * 1000) # 64-bit utc time + self.__content = bytearray(self.VERSION + self.__nonce + + self.__start_time.to_bytes(8, 'little')) + self.__used = False + self.__expired = False + self.__timer = self.__set_timer() + + random.shuffle(self.__content) + + def __repr__(self): + return binascii.hexlify(self.__content).decode("utf-8") + + def __set_timer(self): + interval = self.EXPIRY_TIME / 1000 + timer = threading.Timer(interval, self.__expire_token) + timer.start() + return timer + + def __expire_token(self): + self.__expired = True + self.__timer.cancel() + return None + + def purge(self): + '''Purge the token.''' + self.__used = True + self.__expired = True + self.__content = bytearray() + + def set_as_used(self): + '''Set token as used.''' + self.__used = True + return None + + def get_content(self): + '''Returns token's content value.''' + return self.__content + + def is_valid(self) -> bool: + '''Verifies if token is valid per the evaluation of the expiration + time and its usage flag.''' + period = int(time.time() * 1000) - self.__start_time + if period >= self.EXPIRY_TIME and not self.__expired: + self.__expired = True + + return not (self.__expired or self.__used) + + def compare_tokens(self, token: str) -> bool: + '''Compares token's hex value with a hex string.''' + if len(token) > 0 and all(char in string.hexdigits for char in token): + return (repr(self) == token) + return False diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py index 9dc8a66da3..1af2db6d66 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/utils.py @@ -5,17 +5,14 @@ # from sysinv.common import rest_api from sysinv.ipsec_auth.common import constants -from sysinv.ipsec_auth.common.constants import State from sysinv.common.kubernetes import KUBERNETES_ADMIN_CONF import base64 import fcntl import os -import secrets import socket import struct import subprocess -import time import yaml from cryptography import x509 @@ -36,48 +33,6 @@ from oslo_log import log as logging LOG = logging.getLogger(__name__) -def get_next_state(state): - '''Get the next IPsec Auth state whenever a Stage is finished. - - The IPsec Auth server-client interaction is separated into 5 work stages. - STAGE_1: represents the initial stage where IPsec Auth client send - the first message with OP code, mac address and a hash to - IPsec Auth server. - STAGE_2: represents the stage of validation of the message 1 received - from the client and generation of a response message. If the - validation is satisfied, the IPsec Auth server will encapsulate - an OTS Token, client's hostname, generated public key, - system-local-ca's certificate and a signed hash of this payload - in the response message to send it to the client. - STAGE_3: represents the stage of validation of the message 2 received - from the server and generation of a response message. if the - validation is satisfied, the IPsec Auth Client will encapsulate - an OTS Token, an encrypted Initial Vector (eiv), an encrypted - symetric key (eak1), an encrypted certificate request (eCSR) - and a signed hash of this payload in the response message to - send it to the server. - STAGE_4: represents the stage of validation of the message 3 from the - client and generation of a final response message. If the - validation of the message is satisfied, the IPsec Auth server - will create a CertificateRequest resource with a CSR received - from client's message and will encapsulate the signed - Certificate, network info and a signed hash of this payload in - the response message to send it to the client. - STAGE_5: represents the final stage of IPsec PKI Auth procedure and demands - that IPsec Auth server and client close the connection that - finished STAGE_4. - ''' - if state == State.STAGE_1: - state = State.STAGE_2 - elif state == State.STAGE_2: - state = State.STAGE_3 - elif state == State.STAGE_3: - state = State.STAGE_4 - elif state == State.STAGE_4: - state = State.STAGE_5 - return state - - def get_plataform_conf(param): value = None path = constants.PLATAFORM_CONF_FILE @@ -177,15 +132,6 @@ def save_data(path, data): f.write(data) -def generate_ots_token(): - format = "=b16sQ" # Token format: [b an integer][L unsigned long][L unsigned long] - version = 1 # version - nonce = secrets.token_bytes(16) # 128-bit nonce - utc_time = int(time.time() * 1000) # 64-bit utc time - - return struct.pack(format, version, nonce, utc_time) - - def symmetric_encrypt_data(binary_data, key): iv = os.urandom(16) diff --git a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py index 9cefe2de73..ec8c114147 100644 --- a/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py +++ b/sysinv/sysinv/sysinv/sysinv/ipsec_auth/server/server.py @@ -15,7 +15,8 @@ from sysinv.common import kubernetes from sysinv.common import rest_api from sysinv.ipsec_auth.common import constants from sysinv.ipsec_auth.common import utils -from sysinv.ipsec_auth.common.constants import State +from sysinv.ipsec_auth.common.objects import State +from sysinv.ipsec_auth.common.objects import Token from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization @@ -94,7 +95,7 @@ class IPsecConnection(object): self.signed_cert = None self.tmp_pub_key = None self.tmp_priv_key = None - self.ots_token = None + self.ots_token = Token() self.ca_key = self._get_system_local_ca_secret_info(self.CA_KEY) self.ca_crt = self._get_system_local_ca_secret_info(self.CA_CRT) self.state = State.STAGE_1 @@ -108,18 +109,21 @@ class IPsecConnection(object): if data: # A readable client socket has data LOG.debug("Received {!r}".format(data)) - self.state = utils.get_next_state(self.state) + self.state = State.get_next_state(self.state) LOG.debug("Preparing payload") msg = self._handle_write(data) sock.sendall(msg) - self.state = utils.get_next_state(self.state) + self.state = State.get_next_state(self.state) elif self.state == State.STAGE_5 or not data: + self.ots_token.purge() # Interpret empty result as closed connection LOG.info("Closing connection with {}".format(client_address)) sock.close() sel.unregister(sock) except Exception as e: # Interpret empty result as closed connection + if self.ots_token: + self.ots_token.purge() LOG.exception("%s" % (e)) LOG.error("Closing. {}".format(sock.getpeername())) sock.close() @@ -138,8 +142,8 @@ class IPsecConnection(object): if self.state == State.STAGE_2: LOG.info("Received IPSec Auth request") if not self._validate_client_connection(data): - msg = "Connection refused with client due to invalid info " \ - "received in payload." + msg = ("Connection refused with client due to invalid info " + "received in payload.") raise ConnectionRefusedError(msg) mac_addr = data["mac_addr"] @@ -149,10 +153,10 @@ class IPsecConnection(object): self.mgmt_subnet = client_data['mgmt_subnet'] pub_key = self._generate_tmp_key_pair() - self.ots_token = utils.generate_ots_token() - hash_payload = utils.hash_and_sign_payload(self.ca_key, self.ots_token + pub_key) + token = self.ots_token.get_content() + hash_payload = utils.hash_and_sign_payload(self.ca_key, token + pub_key) - payload["token"] = self.ots_token.hex() + payload["token"] = repr(self.ots_token) payload["hostname"] = self.hostname payload["pub_key"] = pub_key.decode("utf-8") payload["ca_cert"] = self.ca_crt.decode("utf-8") @@ -162,21 +166,31 @@ class IPsecConnection(object): if self.state == State.STAGE_4: LOG.info("Received IPSec Auth CSR request") + token = data["token"] eiv = base64.b64decode(data["eiv"]) eak1 = base64.b64decode(data['eak1']) ecsr = base64.b64decode(data['ecsr']) ehash = base64.b64decode(data['ehash']) + if self.ots_token.compare_tokens(token): + if self.ots_token.is_valid(): + self.ots_token.set_as_used() + else: + raise ValueError("Token expired or already used.") + else: + raise ValueError("Invalid token received.") + + token = self.ots_token.get_content() + + if not utils.verify_encrypted_hash(self.ca_key, ehash, + token, eak1, ecsr): + raise ValueError('Hash validation failed.') + iv = utils.asymmetric_decrypt_data(self.tmp_priv_key, eiv) aes_key = utils.asymmetric_decrypt_data(self.tmp_priv_key, eak1) cert_request = utils.symmetric_decrypt_data(aes_key, iv, ecsr) - if not utils.verify_encrypted_hash(self.ca_key, ehash, - self.ots_token, eak1, ecsr): - raise ValueError('Hash validation failed.') - self.signed_cert = self._sign_cert_request(cert_request) - if not self.signed_cert: raise ValueError('Unable to sign certificate request')