Secure boot support: enabling/disabling and resetting keys

Story: #2008270
Task: #41582
Change-Id: I72f8103fe9bb4613ef8c53a3ff08a710dc96c12d
This commit is contained in:
Dmitry Tantsur 2021-01-13 13:03:40 +01:00
parent e4a3e18de0
commit 5425c005e2
9 changed files with 316 additions and 1 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Adds support for UEFI secure boot: reading the current status, enabling or
disabling secure boot, resetting keys.

View File

@ -25,7 +25,9 @@ class SushyError(Exception):
message = None
def __init__(self, **kwargs):
def __init__(self, message=None, **kwargs):
if message is not None:
self.message = message
if self.message and kwargs:
self.message = self.message % kwargs

View File

@ -173,3 +173,20 @@ SYSTEM_TYPE_VIRTUALLY_PARTITIONED = "VirtuallyPartitioned"
"""A virtual or software-based partition of a computer system"""
SYSTEM_TYPE_COMPOSED = "Composed"
"""A computer system created by binding resource blocks together"""
# Secure boot constants
SECURE_BOOT_ENABLED = "Enabled"
"""UEFI secure boot is enabled."""
SECURE_BOOT_DISABLED = "Disabled"
"""UEFI secure boot is disabled."""
SECURE_BOOT_MODE_SETUP = "SetupMode"
SECURE_BOOT_MODE_USER = "UserMode"
SECURE_BOOT_MODE_AUDIT = "AuditMode"
SECURE_BOOT_MODE_DEPLOYED = "DeployedMode"
SECURE_BOOT_RESET_KEYS_TO_DEFAULT = "ResetAllKeysToDefault"
SECURE_BOOT_RESET_KEYS_DELETE_ALL = "DeleteAllKeys"
SECURE_BOOT_RESET_KEYS_DELETE_PK = "DeletePK"

View File

@ -113,3 +113,27 @@ SYSTEM_TYPE_VALUE_MAP = {
SYSTEM_TYPE_VALUE_MAP_REV = (
utils.revert_dictionary(SYSTEM_TYPE_VALUE_MAP))
SECURE_BOOT_STATE = {
'Enabled': sys_cons.SECURE_BOOT_ENABLED,
'Disabled': sys_cons.SECURE_BOOT_DISABLED,
}
SECURE_BOOT_STATE_REV = utils.revert_dictionary(SECURE_BOOT_STATE)
SECURE_BOOT_MODE = {
'SetupMode': sys_cons.SECURE_BOOT_MODE_SETUP,
'UserMode': sys_cons.SECURE_BOOT_MODE_USER,
'AuditMode': sys_cons.SECURE_BOOT_MODE_AUDIT,
'DeployedMode': sys_cons.SECURE_BOOT_MODE_DEPLOYED,
}
SECURE_BOOT_MODE_REV = utils.revert_dictionary(SECURE_BOOT_MODE)
SECURE_BOOT_RESET_KEYS = {
'ResetAllKeysToDefault': sys_cons.SECURE_BOOT_RESET_KEYS_TO_DEFAULT,
'DeleteAllKeys': sys_cons.SECURE_BOOT_RESET_KEYS_DELETE_ALL,
'DeletePK': sys_cons.SECURE_BOOT_RESET_KEYS_DELETE_PK,
}
SECURE_BOOT_RESET_KEYS_REV = utils.revert_dictionary(SECURE_BOOT_RESET_KEYS)

View File

@ -0,0 +1,123 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# This is referred from Redfish standard schema.
# http://redfish.dmtf.org/schemas/v1/SecureBoot.v1_1_0.json
import logging
from sushy import exceptions
from sushy.resources import base
from sushy.resources import common
from sushy.resources.system import mappings
LOG = logging.getLogger(__name__)
class ResetKeysActionField(common.ActionField):
allowed_values = base.Field('ResetKeysType@Redfish.AllowableValues',
adapter=list)
class ActionsField(base.CompositeField):
reset_keys = ResetKeysActionField('#SecureBoot.ResetKeys')
"""Action that resets the UEFI Secure Boot keys."""
class SecureBoot(base.ResourceBase):
identity = base.Field('Id', required=True)
"""The Bios resource identity string"""
name = base.Field('Name')
"""The name of the resource"""
description = base.Field('Description')
"""Human-readable description of the BIOS resource"""
current_boot = base.MappedField('SecureBootCurrentBoot',
mappings.SECURE_BOOT_STATE)
"""The UEFI Secure Boot state during the current boot cycle."""
enabled = base.Field('SecureBootEnable')
"""Whether the UEFI Secure Boot takes effect on next boot.
This property can be enabled in UEFI boot mode only.
"""
mode = base.MappedField('SecureBootMode', mappings.SECURE_BOOT_MODE)
"""The current UEFI Secure Boot Mode."""
# TODO(dtantsur): SecureBootDatabases
_actions = ActionsField('Actions')
def __init__(self, connector, path, redfish_version=None, registries=None):
"""A class representing secure boot settings.
:param connector: A Connector instance
:param path: Sub-URI path to the SecureBoot resource
:param registries: Dict of message registries to be used when
parsing messages of attribute update status
"""
super().__init__(connector, path, redfish_version, registries)
def _get_reset_action_element(self):
reset_action = self._actions.reset_keys
if not reset_action:
raise exceptions.MissingActionError(action='#SecureBoot.ResetKeys',
resource=self._path)
return reset_action
def get_allowed_reset_keys_values(self):
"""Get the allowed values for resetting the keys.
:returns: A set with the allowed values.
"""
reset_action = self._get_reset_action_element()
if not reset_action.allowed_values:
LOG.warning('Could not figure out the allowed values for the '
'reset keys action for %s', self.identity)
return set(mappings.SECURE_BOOT_RESET_KEYS_REV)
return set([mappings.SECURE_BOOT_RESET_KEYS[v] for v in
set(mappings.SECURE_BOOT_RESET_KEYS).
intersection(reset_action.allowed_values)])
def reset_keys(self, reset_type):
"""Reset secure boot keys.
:param reset_type: Reset type, one of `SECORE_BOOT_RESET_KEYS_*`
constants.
"""
valid_resets = self.get_allowed_reset_keys_values()
if reset_type not in valid_resets:
raise exceptions.InvalidParameterValueError(
parameter='reset_type', value=reset_type,
valid_values=valid_resets)
target_uri = self._get_reset_action_element().target_uri
self._conn.post(target_uri, data={'ResetKeysType': reset_type})
def set_enabled(self, enabled):
"""Enable/disable secure boot.
:param enabled: True, if secure boot is enabled for next boot.
"""
if not isinstance(enabled, bool):
raise exceptions.InvalidParameterValueError(
"Expected a boolean for 'enabled', got %r" % enabled)
self._conn.patch(self.path, data={'SecureBootEnable': enabled})

View File

@ -31,6 +31,7 @@ from sushy.resources.system import constants as sys_cons
from sushy.resources.system import ethernet_interface
from sushy.resources.system import mappings as sys_maps
from sushy.resources.system import processor
from sushy.resources.system import secure_boot
from sushy.resources.system import simple_storage as sys_simple_storage
from sushy.resources.system.storage import storage as sys_storage
from sushy import utils
@ -436,6 +437,21 @@ class System(base.ResourceBase):
registries=self.registries)
for path in paths]
@property
@utils.cache_it
def secure_boot(self):
"""Property to reference `SecureBoot` instance
It is set once when the first time it is queried. On refresh,
this property is marked as stale (greedy-refresh not done).
Here the actual refresh of the sub-resource happens, if stale.
"""
return secure_boot.SecureBoot(
self._conn,
utils.get_sub_resource_path_by(self, 'SecureBoot'),
redfish_version=self.redfish_version,
registries=self.registries)
class SystemCollection(base.ResourceCollectionBase):

View File

@ -0,0 +1,22 @@
{
"@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot",
"@odata.type": "#SecureBoot.v1_1_0.SecureBoot",
"Id": "SecureBoot",
"Name": "UEFI Secure Boot",
"Actions": {
"#SecureBoot.ResetKeys": {
"target": "/redfish/v1/Systems/437XR1138R2/SecureBoot/Actions/SecureBoot.ResetKeys",
"ResetKeysType@Redfish.AllowableValues": [
"ResetAllKeysToDefault",
"DeleteAllKeys",
"DeletePK"
]
}
} ,
"SecureBootEnable": false,
"SecureBootCurrentBoot": "Disabled",
"SecureBootMode": "DeployedMode",
"SecureBootDatabases": {
"@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases"
}
}

View File

@ -0,0 +1,96 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from unittest import mock
from sushy import exceptions
from sushy.resources.system import constants
from sushy.resources.system import secure_boot
from sushy.tests.unit import base
class SecureBootTestCase(base.TestCase):
def setUp(self):
super(SecureBootTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/secure_boot.json') as f:
self.secure_boot_json = json.load(f)
self.conn.get.return_value.json.side_effect = [
self.secure_boot_json
]
self.secure_boot = secure_boot.SecureBoot(
self.conn, '/redfish/v1/Systems/437XR1138R2/SecureBoot',
registries={}, redfish_version='1.1.0')
def test__parse_attributes(self):
self.secure_boot._parse_attributes(self.secure_boot_json)
self.assertEqual('1.1.0', self.secure_boot.redfish_version)
self.assertEqual('SecureBoot', self.secure_boot.identity)
self.assertEqual('UEFI Secure Boot', self.secure_boot.name)
self.assertIsNone(self.secure_boot.description)
self.assertIs(False, self.secure_boot.enabled)
self.assertEqual(constants.SECURE_BOOT_DISABLED,
self.secure_boot.current_boot)
self.assertEqual(constants.SECURE_BOOT_MODE_DEPLOYED,
self.secure_boot.mode)
@mock.patch.object(secure_boot.LOG, 'warning', autospec=True)
def test_get_allowed_reset_keys_values(self, mock_log):
self.assertEqual({constants.SECURE_BOOT_RESET_KEYS_TO_DEFAULT,
constants.SECURE_BOOT_RESET_KEYS_DELETE_ALL,
constants.SECURE_BOOT_RESET_KEYS_DELETE_PK},
self.secure_boot.get_allowed_reset_keys_values())
self.assertFalse(mock_log.called)
@mock.patch.object(secure_boot.LOG, 'warning', autospec=True)
def test_get_allowed_reset_keys_values_no_values(self, mock_log):
self.secure_boot._actions.reset_keys.allowed_values = None
self.assertEqual({constants.SECURE_BOOT_RESET_KEYS_TO_DEFAULT,
constants.SECURE_BOOT_RESET_KEYS_DELETE_ALL,
constants.SECURE_BOOT_RESET_KEYS_DELETE_PK},
self.secure_boot.get_allowed_reset_keys_values())
self.assertTrue(mock_log.called)
@mock.patch.object(secure_boot.LOG, 'warning', autospec=True)
def test_get_allowed_reset_keys_values_custom_values(self, mock_log):
self.secure_boot._actions.reset_keys.allowed_values = [
'ResetAllKeysToDefault',
'IamNotRedfishCompatible',
]
self.assertEqual({constants.SECURE_BOOT_RESET_KEYS_TO_DEFAULT},
self.secure_boot.get_allowed_reset_keys_values())
self.assertFalse(mock_log.called)
def test_set_enabled(self):
self.secure_boot.set_enabled(True)
self.conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/SecureBoot',
data={'SecureBootEnable': True})
def test_set_enabled_wrong_type(self):
self.assertRaises(exceptions.InvalidParameterValueError,
self.secure_boot.set_enabled, 'banana')
def test_reset_keys(self):
self.secure_boot.reset_keys(
constants.SECURE_BOOT_RESET_KEYS_TO_DEFAULT)
self.conn.post.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/SecureBoot'
'/Actions/SecureBoot.ResetKeys',
data={'ResetKeysType': 'ResetAllKeysToDefault'})
def test_reset_keys_wrong_value(self):
self.assertRaises(exceptions.InvalidParameterValueError,
self.secure_boot.reset_keys, 'DeleteEverything')

View File

@ -27,6 +27,7 @@ from sushy.resources.oem import fake
from sushy.resources.system import bios
from sushy.resources.system import mappings as sys_map
from sushy.resources.system import processor
from sushy.resources.system import secure_boot
from sushy.resources.system import simple_storage
from sushy.resources.system import system
from sushy.tests.unit import base
@ -520,6 +521,15 @@ class SystemTestCase(base.TestCase):
self.assertEqual('BIOS Configuration Current Settings',
self.sys_inst.bios.name)
def test_secure_boot(self):
self.conn.get.return_value.json.reset_mock()
with open('sushy/tests/unit/json_samples/secure_boot.json') as f:
self.conn.get.return_value.json.side_effect = [json.load(f)]
self.assertIsInstance(self.sys_inst.secure_boot,
secure_boot.SecureBoot)
self.assertEqual('UEFI Secure Boot', self.sys_inst.secure_boot.name)
def test_simple_storage_for_missing_attr(self):
self.sys_inst.json.pop('SimpleStorage')
with self.assertRaisesRegex(