Merge "OTS Token implementation for IPsec Auth"

This commit is contained in:
Zuul 2024-02-23 17:36:13 +00:00 committed by Gerrit Code Review
commit 7ae03957ff
5 changed files with 153 additions and 82 deletions

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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')