Merge "[Redfish] Enhance removing TLS certificates"

This commit is contained in:
Zuul 2020-08-27 16:00:33 +00:00 committed by Gerrit Code Review
commit 9a29e32bb2
5 changed files with 280 additions and 43 deletions

View File

@ -900,13 +900,15 @@ class IloClient(operations.IloOperations):
def add_tls_certificate(self, cert_file_list): def add_tls_certificate(self, cert_file_list):
"""Adds the TLS certificate to the iLO """Adds the TLS certificate to the iLO
:param cert_file_list: List of TLS certificate files
:raises: IloError, on an error from iLO. :raises: IloError, on an error from iLO.
""" """
return self._call_method('add_tls_certificate', cert_file_list) return self._call_method('add_tls_certificate', cert_file_list)
def remove_tls_certificate(self, fp_list): def remove_tls_certificate(self, cert_file_list):
"""Removes the TLS certificate from the iLO """Removes the TLS certificate from the iLO
:param cert_file_list: List of TLS certificate files
:raises: IloError, on an error from iLO. :raises: IloError, on an error from iLO.
""" """
return self._call_method('remove_tls_certificate', fp_list) return self._call_method('remove_tls_certificate', cert_file_list)

View File

@ -558,10 +558,10 @@ class IloOperations(object):
""" """
raise exception.IloCommandNotSupportedError(ERRMSG) raise exception.IloCommandNotSupportedError(ERRMSG)
def remove_tls_certificate(self, fp_list): def remove_tls_certificate(self, cert_file_list):
"""Removes the TLS certificate from the iLO """Removes the TLS certificate from the iLO
:param fp_list: List of finger prints of the certificates :param cert_file_list: List of TLS certificate files
:raises: IloError, on an error from iLO. :raises: IloError, on an error from iLO.
:raises: IloCommandNotSupportedError, if the command is :raises: IloCommandNotSupportedError, if the command is
not supported on the server. not supported on the server.

View File

@ -14,9 +14,12 @@
__author__ = 'HPE' __author__ = 'HPE'
from base64 import b64decode
import json import json
import re import re
from OpenSSL.crypto import FILETYPE_ASN1
from OpenSSL.crypto import load_certificate
from six.moves.urllib import parse from six.moves.urllib import parse
import sushy import sushy
from sushy.resources.system import mappings as sushy_map from sushy.resources.system import mappings as sushy_map
@ -114,6 +117,9 @@ SUPPORTED_BOOT_MODE_MAP = {
ilo_cons.SUPPORTED_BOOT_MODE_LEGACY_BIOS_AND_UEFI) ilo_cons.SUPPORTED_BOOT_MODE_LEGACY_BIOS_AND_UEFI)
} }
_CERTIFICATE_PATTERN = (
r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----')
LOG = log.get_logger(__name__) LOG = log.get_logger(__name__)
@ -1421,17 +1427,33 @@ class RedfishOperations(operations.IloOperations):
not supported on the server. not supported on the server.
""" """
sushy_system = self._get_sushy_system(PROLIANT_SYSTEM_ID) sushy_system = self._get_sushy_system(PROLIANT_SYSTEM_ID)
if(self._is_boot_mode_uefi()): if(self._is_boot_mode_uefi()):
cert_list = [] cert_list = []
for cert_file in cert_file_list: for cert_file in cert_file_list:
with open(cert_file, 'r') as f: with open(cert_file, 'r') as f:
data = json.dumps(f.read()) data = json.dumps(f.read())
p = re.sub(r"\"", "", data) p = re.sub(r"\"", "", data)
q = re.sub(r"\\n", "\r\n", p) q = re.sub(r"\\n", "\r\n", p).rstrip()
r = q.rstrip()
cert = {} c_list = re.findall(_CERTIFICATE_PATTERN, q, re.DOTALL)
cert['X509Certificate'] = r
cert_list.append(cert) if len(c_list) == 0:
LOG.warning("Could not find any valid certificate in "
"%(cert_file)s. Ignoring." %
{"cert_file": cert_file})
continue
for content in c_list:
cert = {}
cert['X509Certificate'] = content
cert_list.append(cert)
if len(cert_list) == 0:
msg = (self._("No valid certificate in %(cert_file_list)s.") %
{"cert_file_list": cert_file_list})
LOG.debug(msg)
raise exception.IloError(msg)
cert_dict = {} cert_dict = {}
cert_dict['NewCertificates'] = cert_list cert_dict['NewCertificates'] = cert_list
@ -1448,28 +1470,61 @@ class RedfishOperations(operations.IloOperations):
msg = 'TLS certificate cannot be upload in BIOS boot mode' msg = 'TLS certificate cannot be upload in BIOS boot mode'
raise exception.IloCommandNotSupportedInBiosError(msg) raise exception.IloCommandNotSupportedInBiosError(msg)
def remove_tls_certificate(self, fp_list): def remove_tls_certificate(self, cert_file_list):
"""Removes the TLS certificate from the iLO. """Removes the TLS certificate from the iLO.
:param fp_list: List of finger prints of the TLS certificates :param cert_file_list: List of TLS certificate files
:raises: IloError, on an error from iLO. :raises: IloError, on an error from iLO.
:raises: IloCommandNotSupportedError, if the command is :raises: IloCommandNotSupportedError, if the command is
not supported on the server. not supported on the server.
""" """
sushy_system = self._get_sushy_system(PROLIANT_SYSTEM_ID) sushy_system = self._get_sushy_system(PROLIANT_SYSTEM_ID)
if(self._is_boot_mode_uefi()): if(self._is_boot_mode_uefi()):
cert = {} cert_dict = {}
del_cert_list = [] del_cert_list = []
for fp in fp_list: for cert_file in cert_file_list:
cert_fp = { with open(cert_file, 'r') as f:
"FingerPrint": fp data = json.dumps(f.read())
} p = re.sub(r"\"", "", data)
del_cert_list.append(cert_fp) q = re.sub(r"\\n", "\r\n", p).rstrip()
cert.update({"DeleteCertificates": del_cert_list})
c_list = re.findall(_CERTIFICATE_PATTERN, q, re.DOTALL)
if len(c_list) == 0:
LOG.warning("Could not find any valid certificate in "
"%(cert_file)s. Ignoring." %
{"cert_file": cert_file})
continue
for content in c_list:
pem_lines = [line.strip() for line in (
content.strip().split('\n'))]
try:
der_data = b64decode(''.join(pem_lines[1:-1]))
except ValueError:
LOG.warning("Illegal base64 encountered "
"in the certificate.")
else:
cert = load_certificate(FILETYPE_ASN1, der_data)
fp = cert.digest('sha256').decode('ascii')
cert_fp = {
"FingerPrint": fp
}
del_cert_list.append(cert_fp)
if len(del_cert_list) == 0:
msg = (self._("No valid certificate in %(cert_file_list)s.") %
{"cert_file_list": cert_file_list})
raise exception.IloError(msg)
cert_dict.update({"DeleteCertificates": del_cert_list})
try: try:
(sushy_system.bios_settings.tls_config. (sushy_system.bios_settings.tls_config.
tls_config_settings.remove_tls_certificate(cert)) tls_config_settings.remove_tls_certificate(cert_dict))
except sushy.exceptions.SushyError as e: except sushy.exceptions.SushyError as e:
msg = (self._("The Redfish controller has failed to remove " msg = (self._("The Redfish controller has failed to remove "
"TLS certificate. Error %(error)s") % "TLS certificate. Error %(error)s") %

View File

@ -13,7 +13,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import builtins
import collections import collections
import io
import json import json
import ddt import ddt
@ -2050,39 +2053,98 @@ class RedfishOperationsTestCase(testtools.TestCase):
self.rf_client.add_tls_certificate, self.rf_client.add_tls_certificate,
data) data)
@mock.patch.object(builtins, 'open')
@mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi') @mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi')
@mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system')
def test_add_tls_certificate(self, get_sushy_system_mock, def test_add_tls_certificate(self, get_sushy_system_mock,
_uefi_boot_mode_mock): _uefi_boot_mode_mock, open_mock):
_uefi_boot_mode_mock.return_value = True _uefi_boot_mode_mock.return_value = True
cert_file = 'proliantutils/tests/redfish/json_samples/certfile.crt' data = (
with open('proliantutils/tests/redfish/' "-----BEGIN CERTIFICATE-----\nMIID7TC\nCF"
'json_samples/certfile.crt', 'r') as f: "g879\n-----END CERTIFICATE-----\n"
cert_data = f.read() "-----BEGIN CERTIFICATE-----\nKHY8UP\nGH"
"f792\n-----END CERTIFICATE-----\n"
)
import re fd_mock = mock.MagicMock(spec=io.BytesIO)
cert_data = cert_data.rstrip() open_mock.return_value = fd_mock
ref_data = re.sub(r"\n", "\r\n", cert_data) fd_mock.__enter__.return_value = fd_mock
fd_mock.read.return_value = data
c_l = [
"-----BEGIN CERTIFICATE-----\r\nMIID7TC\r\nCF"
"g879\r\n-----END CERTIFICATE-----",
"-----BEGIN CERTIFICATE-----\r\nKHY8UP\r\nGH"
"f792\r\n-----END CERTIFICATE-----"
]
data = { expected_data = {
"NewCertificates": [ "NewCertificates": [
{ {
"X509Certificate": ref_data "X509Certificate": c_l[0],
},
{
"X509Certificate": c_l[1],
} }
] ]
} }
cert_file = '/path/to/certfile'
self.rf_client.add_tls_certificate([cert_file]) self.rf_client.add_tls_certificate([cert_file])
(get_sushy_system_mock.return_value. (get_sushy_system_mock.return_value.
bios_settings.tls_config.tls_config_settings. bios_settings.tls_config.tls_config_settings.
add_tls_certificate.assert_called_once_with(data)) add_tls_certificate.assert_called_once_with(expected_data))
@mock.patch.object(builtins, 'open')
@mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi')
@mock.patch.object(redfish.RedfishOperations, '_get_sushy_system')
def test_add_tls_certificate_no_certificate(self, get_sushy_system_mock,
_uefi_boot_mode_mock,
open_mock):
_uefi_boot_mode_mock.return_value = True
data = (
"-----UNFORMATED CERTIFICATE-----\nMIID7TC\nCF"
"g879\n-----END CERTIFICATE-----\n"
"-----UNFORMATED CERTIFICATE-----\nKHY8UP\nGH"
"f792\n-----END CERTIFICATE-----\n"
)
fd_mock = mock.MagicMock(spec=io.BytesIO)
open_mock.return_value = fd_mock
fd_mock.__enter__.return_value = fd_mock
fd_mock.read.return_value = data
cert_file = "/path/to/certfile"
self.assertRaisesRegex(
exception.IloError,
"No valid certificate",
self.rf_client.add_tls_certificate, [cert_file])
(get_sushy_system_mock.return_value.
bios_settings.tls_config.tls_config_settings.
add_tls_certificate.assert_not_called())
@mock.patch.object(builtins, 'open')
@mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi') @mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi')
@mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system')
def test_add_tls_certificate_raises_ilo_error(self, get_sushy_system_mock, def test_add_tls_certificate_raises_ilo_error(self, get_sushy_system_mock,
_uefi_boot_mode_mock): _uefi_boot_mode_mock,
open_mock):
_uefi_boot_mode_mock.return_value = True _uefi_boot_mode_mock.return_value = True
cert_file = 'proliantutils/tests/redfish/json_samples/certfile.crt'
data = (
"-----BEGIN CERTIFICATE-----\nMIID7TC\nCF"
"g879\n-----END CERTIFICATE-----\n"
"-----BEGIN CERTIFICATE-----\nKHY8UP\nGH"
"f792\n-----END CERTIFICATE-----\n"
)
fd_mock = mock.MagicMock(spec=io.BytesIO)
open_mock.return_value = fd_mock
fd_mock.__enter__.return_value = fd_mock
fd_mock.read.return_value = data
cert_file = '/path/to/certfile'
(get_sushy_system_mock.return_value. (get_sushy_system_mock.return_value.
bios_settings.tls_config.tls_config_settings. bios_settings.tls_config.tls_config_settings.
add_tls_certificate.side_effect) = ( add_tls_certificate.side_effect) = (
@ -2093,26 +2155,143 @@ class RedfishOperationsTestCase(testtools.TestCase):
'The Redfish controller has failed to upload TLS certificate.', 'The Redfish controller has failed to upload TLS certificate.',
self.rf_client.add_tls_certificate, [cert_file]) self.rf_client.add_tls_certificate, [cert_file])
@mock.patch.object(redfish, 'load_certificate')
@mock.patch.object(redfish, 'b64decode')
@mock.patch.object(builtins, 'open')
@mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi') @mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi')
@mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system')
def test_remove_tls_certificate(self, get_sushy_system_mock, def test_remove_tls_certificate(self, get_sushy_system_mock,
_uefi_boot_mode_mock): _uefi_boot_mode_mock, open_mock,
decode_mock, load_cert_mock):
_uefi_boot_mode_mock.return_value = True _uefi_boot_mode_mock.return_value = True
fp = ('FA:3A:68:C7:7E:ED:90:21:D2:FA:3E:54:6B:0C:14:D3:' decode_mock.side_effect = ['first decoded data',
'2F:8D:43:50:F7:05:A7:0F:1C:68:35:DB:5C:D2:53:28') 'second decoded data']
cert1_mock = mock.MagicMock()
cert2_mock = mock.MagicMock()
load_cert_mock.side_effect = [cert1_mock, cert2_mock]
cert1_mock.digest.return_value.decode.return_value = "humptydumpty"
cert2_mock.digest.return_value.decode.return_value = "hickerydickery"
cert = {} data = (
del_cert_list = [] "-----BEGIN CERTIFICATE-----\nMIID7TC\nCF"
cert_fp = { "g879\n-----END CERTIFICATE-----\n"
"FingerPrint": fp "-----BEGIN CERTIFICATE-----\nKHY8UP\nGH"
"f792\n-----END CERTIFICATE-----\n"
)
fd_mock = mock.MagicMock(spec=io.BytesIO)
open_mock.return_value = fd_mock
fd_mock.__enter__.return_value = fd_mock
fd_mock.read.return_value = data
decode_calls = [
mock.call('MIID7TCCFg879'),
mock.call('KHY8UPGHf792')
]
load_cert_calls = [
mock.call(redfish.FILETYPE_ASN1, 'first decoded data'),
mock.call(redfish.FILETYPE_ASN1, 'second decoded data')
]
cert_file = '/path/to/certfile'
self.rf_client.remove_tls_certificate([cert_file])
decode_mock.assert_has_calls(decode_calls)
load_cert_mock.assert_has_calls(load_cert_calls)
expected_data = {
"DeleteCertificates": [
{
"FingerPrint": "humptydumpty",
},
{
"FingerPrint": "hickerydickery"
}
]
} }
del_cert_list.append(cert_fp)
cert.update({"DeleteCertificates": del_cert_list})
self.rf_client.remove_tls_certificate([fp])
(get_sushy_system_mock.return_value. (get_sushy_system_mock.return_value.
bios_settings.tls_config.tls_config_settings. bios_settings.tls_config.tls_config_settings.
remove_tls_certificate.assert_called_once_with(cert)) remove_tls_certificate.assert_called_once_with(expected_data))
@mock.patch.object(redfish, 'load_certificate')
@mock.patch.object(redfish, 'b64decode')
@mock.patch.object(builtins, 'open')
@mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi')
@mock.patch.object(redfish.RedfishOperations, '_get_sushy_system')
def test_remove_tls_certificate_no_certificate(self,
get_sushy_system_mock,
_uefi_boot_mode_mock,
open_mock, decode_mock,
load_cert_mock):
_uefi_boot_mode_mock.return_value = True
data = (
"-----UNFORMATED CERTIFICATE-----\nMIID7TC\nCF"
"g879\n-----END CERTIFICATE-----\n"
"-----UNFORMATED CERTIFICATE-----\nKHY8UP\nGH"
"f792\n-----END CERTIFICATE-----\n"
)
fd_mock = mock.MagicMock(spec=io.BytesIO)
open_mock.return_value = fd_mock
fd_mock.__enter__.return_value = fd_mock
fd_mock.read.return_value = data
cert_file = '/path/to/certfile'
self.assertRaisesRegex(
exception.IloError,
"No valid certificate",
self.rf_client.remove_tls_certificate, [cert_file])
decode_mock.assert_not_called()
load_cert_mock.assert_not_called()
(get_sushy_system_mock.return_value.
bios_settings.tls_config.tls_config_settings.
remove_tls_certificate.assert_not_called())
@mock.patch.object(redfish, 'load_certificate')
@mock.patch.object(redfish, 'b64decode')
@mock.patch.object(builtins, 'open')
@mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi')
@mock.patch.object(redfish.RedfishOperations, '_get_sushy_system')
def test_remove_tls_certificate_raises_ilo_error(self,
get_sushy_system_mock,
_uefi_boot_mode_mock,
open_mock, decode_mock,
load_cert_mock):
_uefi_boot_mode_mock.return_value = True
decode_mock.side_effect = ['first decoded data',
'second decoded data']
cert1_mock = mock.MagicMock()
cert2_mock = mock.MagicMock()
load_cert_mock.side_effect = [cert1_mock, cert2_mock]
cert1_mock.digest.return_value.decode.return_value = "humptydumpty"
cert2_mock.digest.return_value.decode.return_value = "hickerydickery"
data = (
"-----BEGIN CERTIFICATE-----\nMIID7TC\nCF"
"g879\n-----END CERTIFICATE-----\n"
"-----BEGIN CERTIFICATE-----\nKHY8UP\nGH"
"f792\n-----END CERTIFICATE-----\n"
)
fd_mock = mock.MagicMock(spec=io.BytesIO)
open_mock.return_value = fd_mock
fd_mock.__enter__.return_value = fd_mock
fd_mock.read.return_value = data
cert_file = '/path/to/certfile'
(get_sushy_system_mock.return_value.
bios_settings.tls_config.tls_config_settings.
remove_tls_certificate.side_effect) = (
sushy.exceptions.SushyError)
self.assertRaisesRegex(
exception.IloError,
'The Redfish controller has failed to remove TLS certificate.',
self.rf_client.remove_tls_certificate, [cert_file])
@mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi') @mock.patch.object(redfish.RedfishOperations, '_is_boot_mode_uefi')
@mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system')

View File

@ -10,3 +10,4 @@ pysnmp>=4.2.3,<5.0.0 # BSD
# Redfish communication uses the Sushy library # Redfish communication uses the Sushy library
sushy>=3.1.0 sushy>=3.1.0
pyOpenSSL>=19.1.0