Initial implementation of IPsec Auth Server

This commit adds the initial implementation for IPsec Auth Server,
responsible for executing IPsec Auth procedure for an IPsec client
host, by creating and exchanging keys and certificates for trustful
hosts in MGMT network environment.

The IPsec Auth Server should be able to initially perform 2 operations:
initial-auth (OP code 1) and cert-renewal (OP code 2). Those operations
consider connected host informations to proceed with their execution as
the host is recognized as trustful. The keys and certificates generated
in this procedure are used to enable IPsec in communication between two
peers by establishing Security Associations via swanctl configuration.

The main goal of this commit is to create an authentication server to
perform IPsec PKI Auth procedure that remains active and running for
multiple connections requests by different clients in a local network
environment. The IPsec Auth Server should be resilient to maintain open
multiple requests from different clients or procedures in on-going
execution.

Test Plan:
PASS: Build, install and bootstrap an AIO-DX system with a worker node
      associated.
PASS: In a DX system with a worker node associated, login to a
      controller node and execute "ipsec-server -h" command. Observe
      that a help message is displayed in terminal screen specifying
      the command description, arguments and default values that may be
      passed in to the command line.
      All systems in this environment are in enable available active
      statuses.
PASS: In a DX system with a worker node associated, login to a
      controller node and execute "sudo ipsec-server" command. Observe
      that IPsec Auth Server is initiated and waiting for connections.
      All systems in this environment are in enable available active
      statuses. Perform this test with ipsec.service in active and
      inactive status.

Story: 2010940
Task: 49417

Co-Authored-By: Andy Ning <andy.ning@windriver.com>
Co-Authored-By: Leonardo Mendes <Leonardo.MendesSantana@windriver.com>
Signed-off-by: Manoel Benedito Neto <manoel.beneditoneto@windriver.com>
Change-Id: Ic9665e83062ae7bb2d55710e38c48ad8152c36c1
(cherry picked from commit be8dee17f0040c8d6af98747a6b69aec4aa501e0)
This commit is contained in:
Manoel Benedito Neto 2024-01-17 15:30:55 -03:00
parent babe392aa8
commit d989d98bc3
11 changed files with 773 additions and 3 deletions

View File

@ -15,6 +15,7 @@ etc/sysinv/upgrades/delete_load.sh
etc/update-motd.d/10-system
usr/bin/cert-alarm
usr/bin/cert-mon
usr/bin/ipsec-server
usr/bin/kube-cert-rotation.sh
usr/bin/sysinv-agent
usr/bin/sysinv-api

View File

@ -40,6 +40,7 @@ console_scripts =
sysinv-utils = sysinv.cmd.utils:main
cert-mon = sysinv.cmd.cert_mon:main
cert-alarm = sysinv.cmd.cert_alarm:main
ipsec-server = sysinv.cmd.ipsec_server:main
sysinv-reset-n3000-fpgas = sysinv.cmd.reset_n3000_fpgas:main
platform-upgrade = sysinv.cmd.platform:main

View File

@ -16,7 +16,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2013-2023 Wind River Systems, Inc.
# Copyright (c) 2013-2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
@ -1174,6 +1174,7 @@ class HostController(rest.RestController):
'downgrade': ['POST'],
'install_progress': ['POST'],
'wipe_osds': ['GET'],
'update_inv_state': ['POST'],
'kube_upgrade_control_plane': ['POST'],
'kube_upgrade_kubelet': ['POST'],
'device_image_update': ['POST'],
@ -1190,6 +1191,15 @@ class HostController(rest.RestController):
self._api_token = None
# self._name = 'api-host'
@wsme_pecan.wsexpose(six.text_type, wtypes.text, body=six.text_type)
def update_inv_state(self, uuid, data):
try:
pecan.request.dbapi.ihost_update(uuid, {'inv_state': data})
except exception.ServerNotFound:
LOG.error(_('Failed to retrieve host with specified uuid.'))
return False
return True
def _ihosts_get(self, isystem_id, marker, limit, personality,
sort_key, sort_dir):
if self._from_isystem and not isystem_id: # TODO: check uuid

View File

@ -0,0 +1,44 @@
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import argparse
import os
import sys
import textwrap
from sysinv.ipsec_auth.common import constants
from sysinv.ipsec_auth.server.server import IPsecServer
def main():
if not os.geteuid() == 0:
print("%s must be run with root privileges" % (sys.argv[0]))
exit(1)
port = constants.DEFAULT_LISTEN_PORT
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent('''\
Command line interface for IPsec Auth Server.
%(prog)s is used to initialize IPsec Auth Server
and establish connections and IPsec security
associations with other nodes in the cluster through
MGMT network.
'''),
epilog=textwrap.dedent('''\
Note: This command must be run with root privileges.
'''))
parser.add_argument("-p", "--port", metavar='<port>', type=int,
help='Port number (Default: ' + str(port) + ')')
args = parser.parse_args()
if args.port:
port = args.port
server = IPsecServer(port)
server.run()

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2015 Wind River Systems, Inc.
# Copyright (c) 2015-2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -152,7 +152,7 @@ def rest_api_request(token, method, api_cmd, api_cmd_headers=None,
token.set_expired()
LOG.warn("HTTP Error e.code=%s e=%s" % (e.code, e))
if hasattr(e, 'msg') and e.msg:
response = json.loads(e.msg)
response = json.loads('"%s"' % e.msg)
else:
response = json.loads("{}")

View File

@ -0,0 +1,68 @@
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
# 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"
DEFAULT_LISTEN_PORT = 54724
TCP_SERVER = (DEFAULT_BIND_ADDR, DEFAULT_LISTEN_PORT)
PLATAFORM_CONF_FILE = '/etc/platform/platform.conf'
SIOCGIFADDR = 0x8915
SIOCGIFHWADDR = 0x8927
API_VERSION_CERT_MANAGER = 'cert-manager.io/v1'
CERTIFICATE_REQUEST_DURATION = '2160h'
CERTIFICATE_REQUEST_RESOURCE = 'certificaterequests.cert-manager.io'
GROUP_CERT_MANAGER = 'cert-manager.io'
NAMESPACE_CERT_MANAGER = 'cert-manager'
NAMESPACE_DEPLOYMENT = 'deployment'
CLUSTER_ISSUER_SYSTEM_LOCAL_CA = 'system-local-ca'
SECRET_SYSTEM_LOCAL_CA = 'system-local-ca'
TRUSTED_CA_CERT_FILE = 'system-local-ca.crt'
TRUSTED_CA_CERT_DIR = '/etc/swanctl/x509ca/'
TRUSTED_CA_CERT_PATH = TRUSTED_CA_CERT_DIR + TRUSTED_CA_CERT_FILE
CERT_SYSTEM_LOCAL_DIR = '/etc/swanctl/x509/'
CERT_SYSTEM_LOCAL_PRIVATE_DIR = '/etc/swanctl/private/'
CERT_NAME_PREFIX = 'system-ipsec-certificate-'
TMP_DIR_IPSEC = '/tmp/ipsec/'
TMP_DIR_IPSEC_KEYS = TMP_DIR_IPSEC + 'keys/'
TMP_FILE_IPSEC_PUK1 = 'puk1.crt'
TMP_FILE_IPSEC_AK1_KEY = 'ak1.key'
TMP_PUK1_FILE = TMP_DIR_IPSEC + TMP_FILE_IPSEC_PUK1
TMP_AK1_FILE = TMP_DIR_IPSEC_KEYS + TMP_FILE_IPSEC_AK1_KEY
UNIT_HOSTNAME = 'unit_hostname'
FLOATING_UNIT_HOSTNAME = 'floating_unit_hostname'
CONTROLLER = 'controller'
REGION_NAME = 'SystemController'
PXECONTROLLER_URL = 'http://pxecontroller:6385'
OP_CODE_INITIAL_AUTH = 1
OP_CODE_CERT_RENEWAL = 2
OP_CODE_PATCHING = 3
SUPPORTED_OP_CODES = [OP_CODE_INITIAL_AUTH,
OP_CODE_CERT_RENEWAL]
INV_STATE_INVENTORYING = 'inventorying'
INV_STATE_INVENTORIED = 'inventoried'

View File

@ -0,0 +1,343 @@
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
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
from cryptography import exceptions
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import utils
from cryptography.hazmat.primitives.asymmetric import padding as pad
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
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
with open(path) as fp:
lines = fp.readlines()
for line in lines:
if line.find(param) != -1:
value = line.split('=')[1]
value = value.replace('\n', '')
return value
def get_personality():
return get_plataform_conf('nodetype')
def get_management_interface():
return get_plataform_conf('management_interface')
def get_ip_addr(ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ifstruct = struct.pack('256s', bytes(ifname[:15], 'utf-8'))
info = fcntl.ioctl(s.fileno(), constants.SIOCGIFADDR, ifstruct)
return socket.inet_ntoa(info[20:24])
def get_hw_addr(ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ifstruct = struct.pack('256s', bytes(ifname[:15], 'utf-8'))
info = fcntl.ioctl(s.fileno(), constants.SIOCGIFHWADDR, ifstruct)
return ':'.join('%02x' % b for b in info[18:24])
def get_client_hostname_and_mgmt_subnet(mac_addr):
token = rest_api.get_token(constants.REGION_NAME)
sysinv_ihost_url = constants.PXECONTROLLER_URL + '/v1/ihosts/'
api_cmd = sysinv_ihost_url + mac_addr + '/mgmt_ip'
mgmt_info = rest_api.rest_api_request(token, 'GET', api_cmd)
response = {}
if mgmt_info:
hosts = rest_api.rest_api_request(token, 'GET', sysinv_ihost_url)
if not hosts:
raise Exception('Failed to retrieve hosts list.')
personality = None
for h in hosts['ihosts']:
if mac_addr == h['mgmt_mac']:
personality = h['personality']
break
hostname = {}
hostname[constants.UNIT_HOSTNAME] = mgmt_info['hostname']
if personality in constants.CONTROLLER:
hostname[constants.FLOATING_UNIT_HOSTNAME] = constants.CONTROLLER
response['hostname'] = hostname
response['mgmt_subnet'] = mgmt_info['subnet']
return response
def load_data(path):
data = None
with open(path, 'rb') as f:
data = f.read()
return data
def save_data(path, data):
with open(path, 'wb') as f:
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)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend())
padder = padding.PKCS7(algorithms.AES(key).block_size).padder()
binary_data = padder.update(binary_data) + padder.finalize()
encryptor = cipher.encryptor()
encrypted_data = encryptor.update(binary_data) + encryptor.finalize()
return iv, encrypted_data
def symmetric_decrypt_data(aes_key, iv, data):
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), default_backend())
decryptor = cipher.decryptor()
data = decryptor.update(data) + decryptor.finalize()
unpadder = padding.PKCS7(algorithms.AES(aes_key).block_size).unpadder()
decrypted_data = unpadder.update(data) + unpadder.finalize()
return decrypted_data
def asymmetric_encrypt_data(key_data, data, is_cert=False):
if is_cert:
cert = x509.load_pem_x509_certificate(key_data)
key = cert.public_key()
else:
key = serialization.load_pem_public_key(
key_data,
backend=default_backend()
)
return key.encrypt(
data,
pad.OAEP(
mgf=pad.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
def asymmetric_decrypt_data(key, data):
if not isinstance(key, rsa.RSAPrivateKey):
key = serialization.load_pem_private_key(key, None, default_backend())
return key.decrypt(
data,
pad.OAEP(
mgf=pad.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
def hash_payload(payload: dict):
hash_algorithm = hashes.SHA256()
hasher = hashes.Hash(hash_algorithm)
for item in payload.keys():
hasher.update(bytes(payload[item], 'utf-8'))
digest = hasher.finalize()
return digest.hex()
def hash_and_sign_payload(signer, data: bytes):
hasher = hashes.Hash(hashes.SHA256())
hasher.update(data)
digest = hasher.finalize()
key = signer
if not isinstance(key, rsa.RSAPrivateKey):
key = serialization.load_pem_private_key(key, None, default_backend())
data = key.sign(
digest,
pad.PSS(
mgf=pad.MGF1(hashes.SHA256()),
salt_length=pad.PSS.MAX_LENGTH
),
utils.Prehashed(hashes.SHA256())
)
return base64.b64encode(data)
def verify_signed_hash(cert_data, signed_hash, data: bytes):
hasher = hashes.Hash(hashes.SHA256())
hasher.update(data)
digest = hasher.finalize()
cert = x509.load_pem_x509_certificate(cert_data)
key = cert.public_key()
try:
key.verify(
signed_hash,
digest,
pad.PSS(
mgf=pad.MGF1(hashes.SHA256()),
salt_length=pad.PSS.MAX_LENGTH
),
utils.Prehashed(hashes.SHA256())
)
except exceptions.InvalidSignature:
return False
return True
def verify_encrypted_hash(key, ehash, token, eak1, ecsr):
digest = asymmetric_decrypt_data(key, ehash)
hash_algorithm = hashes.SHA256()
hasher = hashes.Hash(hash_algorithm)
hasher.update(bytes(token.hex(), 'utf-8'))
hasher.update(eak1)
hasher.update(ecsr)
hash_value = hasher.finalize()
if digest != hash_value:
return False
return True
def kube_apply_certificate_request(body):
name = body["metadata"]["name"]
# Verify if a CertificateRequest is already created for this specific host
cmd_get = ['kubectl', '--kubeconfig', KUBERNETES_ADMIN_CONF,
'-n', constants.NAMESPACE_DEPLOYMENT, 'get',
constants.CERTIFICATE_REQUEST_RESOURCE, name]
get_cr = subprocess.run(cmd_get, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=False)
# Delete the CertificateRequest if it is already created or check for possible errors
if name in str(get_cr.stdout):
print(f' deleting previously created {name} CertificateRequest.')
cmd_delete = ['kubectl', '--kubeconfig', KUBERNETES_ADMIN_CONF,
'-n', constants.NAMESPACE_DEPLOYMENT, 'delete',
constants.CERTIFICATE_REQUEST_RESOURCE, name]
subprocess.run(cmd_delete, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=False)
elif get_cr.stderr and 'NotFound' not in str(get_cr.stderr):
err = "Error: %s" % (get_cr.stderr.decode("utf-8"))
msg = "Failed to retrieve CertificateRequest resource info. %s" % (err)
raise Exception(msg)
# Create CertificateRequest resource in kubernetes
cr_body = yaml.safe_dump(body, default_flow_style=False)
cmd_apply = ['kubectl', '--kubeconfig', KUBERNETES_ADMIN_CONF,
'apply', '-f', '-']
create_cr = subprocess.run(cmd_apply, input=cr_body.encode(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=False)
if create_cr.stderr:
err = "Error: %s" % (create_cr.stderr.decode("utf-8"))
msg = "Failed to create CertificateRequest %s/%s. %s" \
% (constants.NAMESPACE_DEPLOYMENT, name, err)
raise Exception(msg)
# Get Certificate from recently created resource in kubernetes
cmd_get_certificate = ['-o', "jsonpath='{.status.certificate}'"]
cmd_get_signed_cert = cmd_get + cmd_get_certificate
signed_cert = subprocess.run(cmd_get_signed_cert,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=False)
if signed_cert.stderr:
err = "Error: %s" % (signed_cert.stderr.decode("utf-8"))
msg = "Failed to retrieve %s/%s's Certificate. %s" \
% (constants.NAMESPACE_DEPLOYMENT, name, err)
raise Exception(msg)
return signed_cert.stdout.decode("utf-8").strip("'")

View File

@ -0,0 +1,303 @@
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import base64
import json
import os
import selectors
import socket
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 cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
class IPsecServer(object):
sel = selectors.DefaultSelector()
def __init__(self, port=constants.DEFAULT_LISTEN_PORT):
self.port = port
self.keep_running = True
def run(self):
'''Start accepting connections in TCP server'''
self._create_pid_file()
ssocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ssocket.setblocking(False)
ssocket.bind(constants.TCP_SERVER)
ssocket.listen()
self.sel.register(ssocket, selectors.EVENT_READ, None)
try:
while self.keep_running:
print("waiting for connection...")
for key, _ in self.sel.select(timeout=1):
if key.data is None:
self._accept(key.fileobj)
else:
sock = key.fileobj
connection = key.data
connection.handle_messaging(sock, self.sel)
except KeyboardInterrupt:
print('Server interrupted.')
finally:
print('Shutting down.')
self.sel.close()
def _accept(self, sock):
'''Callback for new connections'''
connection, addr = sock.accept()
connection.setblocking(False)
print(f'accept({addr})')
events = selectors.EVENT_READ
self.sel.register(connection, events, IPsecConnection())
def _create_pid_file(self):
'''Create PID file.'''
pid = str(os.getpid())
pidfile = constants.PROCESS_ID
with open(pidfile, 'w') as f:
f.write(pid)
print(f"PID file created: {pidfile}")
class IPsecConnection(object):
kubeapi = kubernetes.KubeOperator()
CA_KEY = 'tls.key'
CA_CRT = 'tls.crt'
def __init__(self):
self.hostname = None
self.mgmt_subnet = None
self.signed_cert = None
self.tmp_pub_key = None
self.tmp_priv_key = None
self.ots_token = None
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
def handle_messaging(self, sock, sel):
'''Callback for read events'''
try:
client_address = sock.getpeername()
data = sock.recv(4096)
print(' state: %s' % (self.state))
print(f' read({client_address})')
if data:
# A readable client socket has data
print(' received {!r}'.format(data))
self.state = utils.get_next_state(self.state)
print(' changing to state: %s' % (self.state))
print(' preparing payload')
msg = self._handle_write(data)
print(' sending payload')
sock.sendall(msg)
self.state = utils.get_next_state(self.state)
print(' changing to state: %s' % (self.state))
elif self.state == State.STAGE_5 or not data:
# Interpret empty result as closed connection
print(f' closing connection with {client_address}')
sock.close()
sel.unregister(sock)
except Exception as e:
# Interpret empty result as closed connection
print(' %s' % (e))
print(' closing.')
sock.close()
sel.unregister(sock)
def _handle_write(self, recv_message: bytes):
'''Validate received message and generate response message payload to be
sent to the client.'''
try:
data = json.loads(recv_message.decode('utf-8'))
payload = {}
if self.state == State.STAGE_2:
if not self._validate_client_connection(data):
msg = "Connection refused with client due to invalid info " \
"received in payload."
raise ConnectionAbortedError(msg)
mac_addr = data["mac_addr"]
client_data = utils.get_client_hostname_and_mgmt_subnet(mac_addr)
self.hostname = client_data['hostname']
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)
payload["token"] = self.ots_token.hex()
payload["hostname"] = self.hostname
payload["pub_key"] = pub_key.decode("utf-8")
payload["ca_cert"] = self.ca_crt.decode("utf-8")
payload["hash"] = hash_payload.decode("utf-8")
if self.state == State.STAGE_4:
eiv = base64.b64decode(data["eiv"])
eak1 = base64.b64decode(data['eak1'])
ecsr = base64.b64decode(data['ecsr'])
ehash = base64.b64decode(data['ehash'])
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):
msg = "Hash validation failed."
raise ConnectionAbortedError(msg)
self.signed_cert = self._sign_cert_request(cert_request)
cert = bytes(self.signed_cert, 'utf-8')
network = bytes(self.mgmt_subnet, 'utf-8')
hash_payload = utils.hash_and_sign_payload(self.ca_key, cert + network)
payload["cert"] = self.signed_cert
payload["network"] = self.mgmt_subnet
payload["hash"] = hash_payload.decode("utf-8")
payload = json.dumps(payload)
print(f" payload: {payload}")
except AttributeError as e:
raise Exception('Failed to read attribute from payload. Error: %s' % e)
except ConnectionAbortedError as e:
raise Exception('IPsec Server stage failed. Error: %s' % e)
except ValueError as e:
raise Exception('Failed to decode message. Error: %s' % e)
except TypeError as e:
raise Exception('Failed to read values from payload. '
'Values of attributes must be str. Error: %s' % e)
except Exception as e:
raise Exception('An unknown exception occurred. Error: %s' % e)
return bytes(payload, "utf-8")
def _validate_client_connection(self, message):
hashed_item = message.pop('hash')
hashed_payload = utils.hash_payload(message)
if hashed_item != hashed_payload:
print(' Inconsistent hash of payload.')
return False
op_code = int(message["op"])
if op_code not in constants.SUPPORTED_OP_CODES:
print(' Operation not supported.')
return False
token = rest_api.get_token(constants.REGION_NAME)
sysinv_ihost_url = constants.PXECONTROLLER_URL + '/v1/ihosts/'
hosts_info = rest_api.rest_api_request(token, 'GET', sysinv_ihost_url)
if not hosts_info:
print(' Failed to retrieve hosts list.')
return False
uuid = None
inv_state = None
mgmt_mac = None
personality = None
for h in hosts_info['ihosts']:
if message["mac_addr"] == h['mgmt_mac']:
uuid = h['uuid']
inv_state = h['inv_state']
mgmt_mac = h['mgmt_mac']
personality = h['personality']
break
if not uuid or not mgmt_mac or not personality or \
op_code == constants.OP_CODE_INITIAL_AUTH and inv_state != '' or \
op_code == constants.OP_CODE_CERT_RENEWAL and \
inv_state != constants.INV_STATE_INVENTORIED:
print(' Invalid host information.')
return False
if op_code == constants.OP_CODE_INITIAL_AUTH and inv_state == '':
api_cmd = sysinv_ihost_url + uuid + '/update_inv_state'
api_cmd_payload = '"{}"'.format(constants.INV_STATE_INVENTORYING)
api_cmd_headers = dict()
api_cmd_headers['Content-type'] = "application/json"
api_cmd_headers['User-Agent'] = "sysinv/1.0"
if not rest_api.rest_api_request(token, "POST", api_cmd,
api_cmd_headers=api_cmd_headers,
api_cmd_payload=api_cmd_payload):
print(' Failed to update host inventory state.')
return False
return True
def _generate_tmp_key_pair(self):
'''Generate a temporary key pair to encrypt and decrypt exchanged data.'''
self.tmp_priv_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
self.tmp_pub_key = self.tmp_priv_key.public_key()
pub_key_bytes = self.tmp_pub_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return base64.b64encode(pub_key_bytes)
def _get_system_local_ca_secret_info(self, attr):
'''Retrieve system-local-ca's private key.'''
secret = self.kubeapi.kube_get_secret(constants.SECRET_SYSTEM_LOCAL_CA,
constants.NAMESPACE_CERT_MANAGER)
if not secret:
raise Exception("TLS secret is unreachable.")
data = bytes(secret.data.get(attr, None), "utf-8")
if not data:
raise Exception(f"Failed to retrieve system-local-ca's {attr} info.")
if attr == self.CA_KEY:
data = base64.b64decode(data)
return data
def _sign_cert_request(self, request):
'''Create CertificateRequest related to a specific host's CSR and retrieve
the signed certificate.'''
csr_name = constants.CERT_NAME_PREFIX + self.hostname[constants.UNIT_HOSTNAME]
csr_request = base64.b64encode(request).decode("utf-8")
csr_body = {
"apiVersion": constants.API_VERSION_CERT_MANAGER,
"kind": "CertificateRequest",
"metadata": {
"name": csr_name,
"namespace": constants.NAMESPACE_DEPLOYMENT,
},
"spec": {
"request": csr_request,
"isCA": False,
"usages": ["signing", "digital signature", "server auth"],
"duration": constants.CERTIFICATE_REQUEST_DURATION,
"issuerRef": {
"name": constants.CLUSTER_ISSUER_SYSTEM_LOCAL_CA,
"kind": "ClusterIssuer",
"group": constants.GROUP_CERT_MANAGER,
},
},
}
return utils.kube_apply_certificate_request(csr_body)