Merge "Initial support for secure boot databases"

This commit is contained in:
Zuul 2021-03-01 15:43:07 +00:00 committed by Gerrit Code Review
commit d4c7e0395b
9 changed files with 395 additions and 3 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Adds support for fetching and resetting individual UEFI secure boot
databases.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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