diff --git a/releasenotes/notes/secure-boot-database-7fae673722d7cf4f.yaml b/releasenotes/notes/secure-boot-database-7fae673722d7cf4f.yaml new file mode 100644 index 00000000..6b60f94a --- /dev/null +++ b/releasenotes/notes/secure-boot-database-7fae673722d7cf4f.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for fetching and resetting individual UEFI secure boot + databases. diff --git a/sushy/resources/system/constants.py b/sushy/resources/system/constants.py index 4bb5251a..92a8be00 100644 --- a/sushy/resources/system/constants.py +++ b/sushy/resources/system/constants.py @@ -190,3 +190,17 @@ 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" + +SECURE_BOOT_PLATFORM_KEY = "PK" +SECURE_BOOT_KEY_EXCHANGE_KEYS = "KEK" +SECURE_BOOT_ALLOWED_KEYS_DATABASE = "db" +SECURE_BOOT_DENIED_KEYS_DATABASE = "dbx" +SECURE_BOOT_RECOVERY_KEYS_DATABASE = "dbr" +SECURE_BOOT_TIMESTAMP_DATABASE = "dbt" + +SECURE_BOOT_DEFAULT_PLATFORM_KEY = "PKDefault" +SECURE_BOOT_DEFAULT_KEY_EXCHANGE_KEYS = "KEKDefault" +SECURE_BOOT_DEFAULT_ALLOWED_KEYS_DATABASE = "dbDefault" +SECURE_BOOT_DEFAULT_DENIED_KEYS_DATABASE = "dbxDefault" +SECURE_BOOT_DEFAULT_RECOVERY_KEYS_DATABASE = "dbrDefault" +SECURE_BOOT_DEFAULT_TIMESTAMP_DATABASE = "dbtDefault" diff --git a/sushy/resources/system/mappings.py b/sushy/resources/system/mappings.py index 3c354145..5e77b2bd 100644 --- a/sushy/resources/system/mappings.py +++ b/sushy/resources/system/mappings.py @@ -137,3 +137,29 @@ SECURE_BOOT_RESET_KEYS = { } SECURE_BOOT_RESET_KEYS_REV = utils.revert_dictionary(SECURE_BOOT_RESET_KEYS) + +SECURE_BOOT_DATABASE_TYPE = { + 'PK': sys_cons.SECURE_BOOT_PLATFORM_KEY, + 'KEK': sys_cons.SECURE_BOOT_KEY_EXCHANGE_KEYS, + 'db': sys_cons.SECURE_BOOT_ALLOWED_KEYS_DATABASE, + 'dbx': sys_cons.SECURE_BOOT_DENIED_KEYS_DATABASE, + 'dbr': sys_cons.SECURE_BOOT_RECOVERY_KEYS_DATABASE, + 'dbt': sys_cons.SECURE_BOOT_TIMESTAMP_DATABASE, + 'PKDefault': sys_cons.SECURE_BOOT_DEFAULT_PLATFORM_KEY, + 'KEKDefault': sys_cons.SECURE_BOOT_DEFAULT_KEY_EXCHANGE_KEYS, + 'dbDefault': sys_cons.SECURE_BOOT_DEFAULT_ALLOWED_KEYS_DATABASE, + 'dbxDefault': sys_cons.SECURE_BOOT_DEFAULT_DENIED_KEYS_DATABASE, + 'dbrDefault': sys_cons.SECURE_BOOT_DEFAULT_RECOVERY_KEYS_DATABASE, + 'dbtDefault': sys_cons.SECURE_BOOT_DEFAULT_TIMESTAMP_DATABASE, +} + +SECURE_BOOT_DATABASE_TYPE_REV = utils.revert_dictionary( + SECURE_BOOT_DATABASE_TYPE) + +SECURE_BOOT_DATABASE_RESET_KEYS = { + 'ResetAllKeysToDefault': sys_cons.SECURE_BOOT_RESET_KEYS_TO_DEFAULT, + 'DeleteAllKeys': sys_cons.SECURE_BOOT_RESET_KEYS_DELETE_ALL, +} + +SECURE_BOOT_DATABASE_RESET_KEYS_REV = utils.revert_dictionary( + SECURE_BOOT_DATABASE_RESET_KEYS) diff --git a/sushy/resources/system/secure_boot.py b/sushy/resources/system/secure_boot.py index ab07762f..aa6136f7 100644 --- a/sushy/resources/system/secure_boot.py +++ b/sushy/resources/system/secure_boot.py @@ -19,6 +19,8 @@ from sushy import exceptions from sushy.resources import base from sushy.resources import common from sushy.resources.system import mappings +from sushy.resources.system import secure_boot_database +from sushy import utils LOG = logging.getLogger(__name__) @@ -73,6 +75,25 @@ class SecureBoot(base.ResourceBase): """ super().__init__(connector, path, redfish_version, registries) + @property + @utils.cache_it + def databases(self): + """A collection of secure boot databases. + + 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. + + :raises: MissingAttributeError if 'SecureBootDatabases/@odata.id' field + is missing. + :returns: `SimpleStorageCollection` instance + """ + return secure_boot_database.SecureBootDatabaseCollection( + self._conn, utils.get_sub_resource_path_by( + self, "SecureBootDatabases"), + redfish_version=self.redfish_version, + registries=self.registries) + def _get_reset_action_element(self): reset_action = self._actions.reset_keys if not reset_action: diff --git a/sushy/resources/system/secure_boot_database.py b/sushy/resources/system/secure_boot_database.py new file mode 100644 index 00000000..aa4e9b11 --- /dev/null +++ b/sushy/resources/system/secure_boot_database.py @@ -0,0 +1,112 @@ +# 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 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('#SecureBootDatabase.ResetKeys') + """Action that resets the UEFI Secure Boot keys.""" + + +class SecureBootDatabase(base.ResourceBase): + + # TODO(dtantsur): certificates + + database_id = base.MappedField('DatabaseId', + mappings.SECURE_BOOT_DATABASE_TYPE) + """Standard UEFI database type.""" + + description = base.Field('Description') + """The system description""" + + identity = base.Field('Id', required=True) + """The secure boot database identity string""" + + name = base.Field('Name') + """The secure boot database name""" + + # TODO(dtantsur): signatures + + _actions = ActionsField('Actions') + + def _get_reset_action_element(self): + reset_action = self._actions.reset_keys + if not reset_action: + raise exceptions.MissingActionError( + action='#SecureBootDatabase.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_DATABASE_RESET_KEYS_REV) + + return set([mappings.SECURE_BOOT_DATABASE_RESET_KEYS[v] for v in + set(mappings.SECURE_BOOT_DATABASE_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}) + + +class SecureBootDatabaseCollection(base.ResourceCollectionBase): + + @property + def _resource_type(self): + return SecureBootDatabase + + def __init__(self, connector, path, redfish_version=None, registries=None): + """A class representing a ComputerSystemCollection + + :param connector: A Connector instance + :param path: The canonical path to the System collection resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + :param registries: Dict of Redfish Message Registry objects to be + used in any resource that needs registries to parse messages + """ + super(SecureBootDatabaseCollection, self).__init__( + connector, path, redfish_version, registries) diff --git a/sushy/tests/unit/json_samples/secure_boot_database.json b/sushy/tests/unit/json_samples/secure_boot_database.json new file mode 100644 index 00000000..53e7e94f --- /dev/null +++ b/sushy/tests/unit/json_samples/secure_boot_database.json @@ -0,0 +1,26 @@ +{ + "@odata.type": "#SecureBootDatabase.v1_0_0.SecureBootDatabase", + "Id": "db", + "Name": "db - Authorized Signature Database", + "Description": "UEFI db Secure Boot Database", + "DatabaseId": "db", + "Certificates": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/db/Certificates/" + }, + "Signatures": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/db/Signatures/" + }, + "Actions": { + "#SecureBootDatabase.ResetKeys": { + "target": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/db/Actions/SecureBootDatabase.ResetKeys", + "ResetKeysType@Redfish.AllowableValues": [ + "ResetAllKeysToDefault", + "DeleteAllKeys" + ] + }, + "Oem": {} + }, + "Oem": {}, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/db", + "@Redfish.Copyright": "Copyright 2014-2021 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/json_samples/secure_boot_database_collection.json b/sushy/tests/unit/json_samples/secure_boot_database_collection.json new file mode 100644 index 00000000..ed6b22b2 --- /dev/null +++ b/sushy/tests/unit/json_samples/secure_boot_database_collection.json @@ -0,0 +1,34 @@ +{ + "@odata.type": "#SecureBootDatabaseCollection.SecureBootDatabaseCollection", + "Name": "UEFI SecureBoot Database Collection", + "Members@odata.count": 8, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/PK" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/KEK" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/db" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/dbx" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/PKDefault" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/KEKDefault" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/dbDefault" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/dbxDefault" + } + ], + "Oem": {}, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases", + "@Redfish.Copyright": "Copyright 2014-2021 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/resources/system/test_secure_boot.py b/sushy/tests/unit/resources/system/test_secure_boot.py index 2e61592f..b189e72c 100644 --- a/sushy/tests/unit/resources/system/test_secure_boot.py +++ b/sushy/tests/unit/resources/system/test_secure_boot.py @@ -16,6 +16,7 @@ from unittest import mock from sushy import exceptions from sushy.resources.system import constants from sushy.resources.system import secure_boot +from sushy.resources.system import secure_boot_database from sushy.tests.unit import base @@ -27,9 +28,7 @@ class SecureBootTestCase(base.TestCase): 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.conn.get.return_value.json.return_value = self.secure_boot_json self.secure_boot = secure_boot.SecureBoot( self.conn, '/redfish/v1/Systems/437XR1138R2/SecureBoot', registries={}, redfish_version='1.1.0') @@ -94,3 +93,20 @@ class SecureBootTestCase(base.TestCase): def test_reset_keys_wrong_value(self): self.assertRaises(exceptions.InvalidParameterValueError, self.secure_boot.reset_keys, 'DeleteEverything') + + def test_databases(self): + self.conn.get.return_value.json.reset_mock() + + with open('sushy/tests/unit/json_samples/' + 'secure_boot_database_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + result = self.secure_boot.databases + self.assertIsInstance( + result, secure_boot_database.SecureBootDatabaseCollection) + self.conn.get.return_value.json.assert_called_once_with() + + self.conn.get.return_value.json.reset_mock() + + self.assertIs(result, self.secure_boot.databases) + self.conn.get.return_value.json.assert_not_called() diff --git a/sushy/tests/unit/resources/system/test_secure_boot_database.py b/sushy/tests/unit/resources/system/test_secure_boot_database.py new file mode 100644 index 00000000..714c8eff --- /dev/null +++ b/sushy/tests/unit/resources/system/test_secure_boot_database.py @@ -0,0 +1,138 @@ +# 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_database +from sushy.tests.unit import base + + +class SecureBootDatabaseTestCase(base.TestCase): + + def setUp(self): + super(SecureBootDatabaseTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'secure_boot_database.json') as f: + self.secure_boot_json = json.load(f) + + self.conn.get.return_value.json.return_value = self.secure_boot_json + self.secure_boot = secure_boot_database.SecureBootDatabase( + self.conn, + '/redfish/v1/Systems/437XR1138R2/SecureBoot' + '/SecureBootDatabases/db', + registries={}, redfish_version='1.0.0') + + def test__parse_attributes(self): + self.secure_boot._parse_attributes(self.secure_boot_json) + self.assertEqual('1.0.0', self.secure_boot.redfish_version) + self.assertEqual('db', self.secure_boot.identity) + self.assertEqual('db - Authorized Signature Database', + self.secure_boot.name) + + @mock.patch.object(secure_boot_database.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}, + self.secure_boot.get_allowed_reset_keys_values()) + self.assertFalse(mock_log.called) + + @mock.patch.object(secure_boot_database.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}, + self.secure_boot.get_allowed_reset_keys_values()) + self.assertTrue(mock_log.called) + + @mock.patch.object(secure_boot_database.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_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/SecureBootDatabases/db' + '/Actions/SecureBootDatabase.ResetKeys', + data={'ResetKeysType': 'ResetAllKeysToDefault'}) + + def test_reset_keys_wrong_value(self): + self.assertRaises(exceptions.InvalidParameterValueError, + self.secure_boot.reset_keys, 'DeleteEverything') + + +class SecureBootDatabaseCollectionTestCase(base.TestCase): + + def setUp(self): + super(SecureBootDatabaseCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'secure_boot_database_collection.json') as f: + self.json_doc = json.load(f) + + self.conn.get.return_value.json.return_value = self.json_doc + + self.collection = secure_boot_database.SecureBootDatabaseCollection( + self.conn, '/redfish/v1/Systems/437XR1138R2/SecureBootDatabases', + redfish_version='1.0.0') + + def test__parse_attributes(self): + self.collection._parse_attributes(self.json_doc) + self.assertEqual('1.0.0', self.collection.redfish_version) + self.assertEqual('UEFI SecureBoot Database Collection', + self.collection.name) + self.assertEqual(tuple( + '/redfish/v1/Systems/437XR1138R2/SecureBoot/SecureBootDatabases/' + + member + for member in ('PK', 'KEK', 'db', 'dbx', + 'PKDefault', 'KEKDefault', + 'dbDefault', 'dbxDefault') + ), self.collection.members_identities) + + @mock.patch.object(secure_boot_database, 'SecureBootDatabase', + autospec=True) + def test_get_member(self, mock_secure_boot_database): + self.collection.get_member( + '/redfish/v1/Systems/437XR1138R2/SecureBoot' + '/SecureBootDatabases/db') + mock_secure_boot_database.assert_called_once_with( + self.collection._conn, + '/redfish/v1/Systems/437XR1138R2/SecureBoot' + '/SecureBootDatabases/db', + self.collection.redfish_version, None) + + @mock.patch.object(secure_boot_database, 'SecureBootDatabase', + autospec=True) + def test_get_members(self, mock_secure_boot_database): + members = self.collection.get_members() + calls = [ + mock.call(self.collection._conn, + '/redfish/v1/Systems/437XR1138R2/SecureBoot' + '/SecureBootDatabases/%s' % member, + self.collection.redfish_version, None) + for member in ('PK', 'KEK', 'db', 'dbx', + 'PKDefault', 'KEKDefault', + 'dbDefault', 'dbxDefault') + ] + mock_secure_boot_database.assert_has_calls(calls) + self.assertIsInstance(members, list) + self.assertEqual(8, len(members))