Merge "OTS Token implementation for IPsec Auth"
This commit is contained in:
commit
7ae03957ff
@ -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
|
||||
|
@ -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"
|
||||
|
122
sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/objects.py
Normal file
122
sysinv/sysinv/sysinv/sysinv/ipsec_auth/common/objects.py
Normal 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
|
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user