From 559d74004bc9ba918b895dd1b3177b08f2ce0069 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Thu, 19 Sep 2024 02:33:34 +1200 Subject: [PATCH] Middleware SGX admin tooling (#199) * Added onboard method to HSM2DongleSGX * Added Platform abstraction to manage common platform parameters * Replaced protocol platform-specific configuration for text messages with Platform messages * Implemented SGX admin tool - Implemented main SGX admin tool entrypoint with onboard, unlock, pin change and public key gathering commands - Misc get_hsm function now accounts for current Platform to acquire a connection - Updated specific admin modules to account for difference between platforms for specific operations - Now setting Platform within Ledger admin tool * Updated build scripts to account for SGX manager and admin * Added and updated unit tests --- middleware/adm_ledger.py | 4 +- middleware/adm_sgx.py | 128 +++++++++++++++++ middleware/admin/changepin.py | 14 +- middleware/admin/misc.py | 10 +- middleware/admin/onboard.py | 17 ++- middleware/admin/pubkeys.py | 3 +- middleware/admin/unlock.py | 8 +- middleware/build/adm_sgx | 2 + middleware/build/all | 2 + middleware/comm/platform.py | 65 +++++++++ middleware/ledger/protocol.py | 12 +- middleware/manager_ledger.py | 10 +- middleware/manager_sgx.py | 10 +- middleware/manager_tcp.py | 11 +- middleware/mgr/runner.py | 5 +- middleware/sgx/hsm2dongle.py | 18 +++ middleware/tests/admin/test_adm_sgx.py | 171 +++++++++++++++++++++++ middleware/tests/admin/test_changepin.py | 19 ++- middleware/tests/admin/test_onboard.py | 51 ++++++- middleware/tests/admin/test_unlock.py | 2 + 20 files changed, 508 insertions(+), 54 deletions(-) create mode 100644 middleware/adm_sgx.py create mode 100755 middleware/build/adm_sgx create mode 100644 middleware/comm/platform.py create mode 100644 middleware/tests/admin/test_adm_sgx.py diff --git a/middleware/adm_ledger.py b/middleware/adm_ledger.py index 4865341b..da484038 100644 --- a/middleware/adm_ledger.py +++ b/middleware/adm_ledger.py @@ -24,6 +24,7 @@ from argparse import ArgumentParser import logging from ledger.hsm2dongle import HSM2DongleError +from comm.platform import Platform from admin.misc import not_implemented, info, AdminError from admin.unlock import do_unlock from admin.onboard import do_onboard @@ -33,8 +34,6 @@ from admin.verify_attestation import do_verify_attestation from admin.authorize_signer import do_authorize_signer -DEFAULT_PIN_FILE = "pin.txt" -DEFAULT_PIN_CHANGE_FILE = "changePIN" DEFAULT_ATT_UD_SOURCE = "https://public-node.rsk.co" @@ -144,6 +143,7 @@ def main(): try: options = parser.parse_args() + Platform.set(Platform.LEDGER) actions.get(options.operation, not_implemented)(options) sys.exit(0) except AdminError as e: diff --git a/middleware/adm_sgx.py b/middleware/adm_sgx.py new file mode 100644 index 00000000..6a4dcf0c --- /dev/null +++ b/middleware/adm_sgx.py @@ -0,0 +1,128 @@ +# The MIT License (MIT) +# +# Copyright (c) 2021 RSK Labs Ltd +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys +from argparse import ArgumentParser +import logging +from ledger.hsm2dongle import HSM2DongleError +from comm.platform import Platform +from admin.misc import not_implemented, info, AdminError +from admin.unlock import do_unlock +from admin.onboard import do_onboard +from admin.pubkeys import do_get_pubkeys +from admin.changepin import do_changepin + + +def main(): + logging.disable(logging.CRITICAL) + + actions = { + "unlock": do_unlock, + "onboard": do_onboard, + "pubkeys": do_get_pubkeys, + "changepin": do_changepin, + } + + parser = ArgumentParser(description="SGX powHSM Administrative tool") + parser.add_argument("operation", choices=list(actions.keys())) + parser.add_argument( + "-r", + "--port", + dest="sgx_port", + help="SGX powHSM listening port (default 7777)", + type=int, + default=7777, + ) + parser.add_argument( + "-s", + "--host", + dest="sgx_host", + help="SGX powHSM host. (default 'localhost')", + default="localhost", + ) + parser.add_argument("-p", "--pin", dest="pin", help="PIN.") + parser.add_argument( + "-n", + "--newpin", + dest="new_pin", + help="New PIN (only valid for 'changepin' operation).", + ) + parser.add_argument( + "-a", + "--anypin", + dest="any_pin", + action="store_const", + help="Allow any pin (only valid for 'changepin' operation).", + default=False, + const=True, + ) + parser.add_argument( + "-o", + "--output", + dest="output_file_path", + help="Output file (only valid for 'onboard' and 'pubkeys' " + "operations).", + ) + parser.add_argument( + "-u", + "--nounlock", + dest="no_unlock", + action="store_const", + help="Do not attempt to unlock (only valid for 'changepin' and 'pubkeys' " + "operations).", + default=False, + const=True, + ) + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="store_const", + help="Enable verbose mode", + default=False, + const=True, + ) + + try: + options = parser.parse_args() + Platform.set(Platform.SGX, { + "sgx_host": options.sgx_host, + "sgx_port": options.sgx_port, + }) + actions.get(options.operation, not_implemented)(options) + sys.exit(0) + except AdminError as e: + info(str(e)) + sys.exit(1) + except HSM2DongleError as e: + info(str(e)) + sys.exit(2) + except KeyboardInterrupt: + info("Interrupted by user!") + sys.exit(3) + except Exception as e: + info(str(e)) + sys.exit(4) + + +if __name__ == "__main__": + main() diff --git a/middleware/admin/changepin.py b/middleware/admin/changepin.py index 61be97c5..78ae4230 100644 --- a/middleware/admin/changepin.py +++ b/middleware/admin/changepin.py @@ -32,6 +32,7 @@ AdminError, ) from .unlock import do_unlock +from comm.platform import Platform def do_changepin(options): @@ -62,10 +63,10 @@ def do_changepin(options): mode = hsm.get_current_mode() info(f"Mode: {mode.name.capitalize()}") - # We can only change the pin while in bootloader mode - if mode != HSM2Dongle.MODE.BOOTLOADER: - raise AdminError("Device not in bootloader mode. Disconnect and re-connect the " - "ledger and try again") + # In Ledger, we can only change the pin while in bootloader mode + if Platform.is_ledger() and mode != HSM2Dongle.MODE.BOOTLOADER: + raise AdminError("Device not in bootloader mode. " + f"{Platform.message("restart").capitalize()} and try again") # Ask the user for a new pin if one has not been given if new_pin is None: @@ -76,6 +77,9 @@ def do_changepin(options): info("Changing pin... ", options.verbose) if not hsm.new_pin(new_pin): raise AdminError("Failed to change pin") - info("Pin changed. Please disconnect and re-connect the ledger.") + info("Pin changed.", nl=Platform.is_sgx()) + # We require a restart in Ledger only + if Platform.is_ledger(): + info(f" Please {Platform.message("restart")}.") dispose_hsm(hsm) diff --git a/middleware/admin/misc.py b/middleware/admin/misc.py index b0780aa5..c8dcd59b 100644 --- a/middleware/admin/misc.py +++ b/middleware/admin/misc.py @@ -24,9 +24,11 @@ import time from getpass import getpass from ledger.hsm2dongle import HSM2Dongle +from sgx.hsm2dongle import HSM2DongleSGX from ledger.pin import BasePin from .dongle_admin import DongleAdmin from .dongle_eth import DongleEth +from comm.platform import Platform PIN_ERROR_MESSAGE = ("Invalid pin given. It must be exactly 8 alphanumeric " "characters with at least one alphabetic character.") @@ -68,7 +70,13 @@ def not_implemented(options): def get_hsm(debug): info("Connecting to HSM... ", False) - hsm = HSM2Dongle(debug) + if Platform.is_ledger(): + hsm = HSM2Dongle(debug) + elif Platform.is_sgx(): + hsm = HSM2DongleSGX(Platform.options("sgx_host"), + Platform.options("sgx_port"), debug) + else: + raise AdminError("Platform not set or unknown platform") hsm.connect() info("OK") return hsm diff --git a/middleware/admin/onboard.py b/middleware/admin/onboard.py index f1a2df05..7eb480b9 100644 --- a/middleware/admin/onboard.py +++ b/middleware/admin/onboard.py @@ -39,6 +39,7 @@ from .dongle_admin import DongleAdmin from .unlock import do_unlock from .certificate import HSMCertificate, HSMCertificateElement +from comm.platform import Platform # TODO: this could perhaps be done with a different value. # Currently unused but necessary for the attestation setup process. @@ -50,8 +51,8 @@ def do_onboard(options): head("### -> Onboarding and attestation setup", fill="#") hsm = None - # Require an output file - if options.output_file_path is None: + # Ledger-only: require an output file + if Platform.is_ledger() and options.output_file_path is None: raise AdminError("No output file path given") # Validate pin (if given) @@ -71,8 +72,8 @@ def do_onboard(options): # Require bootloader mode for onboarding if mode != HSM2Dongle.MODE.BOOTLOADER: - raise AdminError("Device not in bootloader mode. Disconnect and re-connect the " - "ledger and try again") + raise AdminError("Device not in bootloader mode. " + f"{Platform.message("restart").capitalize()} and try again") # Echo check info("Sending echo... ", options.verbose) @@ -120,6 +121,14 @@ def do_onboard(options): dispose_hsm(hsm) + if Platform.is_sgx(): + head(["Onboarding done"]) + return + + if not Platform.is_ledger(): + raise AdminError("Unsupported platform") + + # **** Attestation setup is Ledger only (for now) **** head( [ "Onboarding done", diff --git a/middleware/admin/pubkeys.py b/middleware/admin/pubkeys.py index 38b42490..cb49d457 100644 --- a/middleware/admin/pubkeys.py +++ b/middleware/admin/pubkeys.py @@ -27,6 +27,7 @@ from .misc import info, get_hsm, dispose_hsm, AdminError, wait_for_reconnection from .unlock import do_unlock from comm.bip32 import BIP32Path +from comm.platform import Platform SIGNER_WAIT_TIME = 1 # second @@ -65,7 +66,7 @@ def do_get_pubkeys(options): # Modes for which we can't get the public keys if mode in [HSM2Dongle.MODE.UNKNOWN, HSM2Dongle.MODE.BOOTLOADER]: raise AdminError( - "Device not in app mode. Disconnect and re-connect the ledger and try again") + f"Device not in app mode. {Platform.message("restart").capitalize()}") # Gather public keys pubkeys = {} diff --git a/middleware/admin/unlock.py b/middleware/admin/unlock.py index 6c9c8e9a..61ca64cf 100644 --- a/middleware/admin/unlock.py +++ b/middleware/admin/unlock.py @@ -32,6 +32,7 @@ AdminError, ask_for_pin, ) +from comm.platform import Platform def do_unlock(options, exit=True, no_exec=False, label=True): @@ -65,8 +66,8 @@ def do_unlock(options, exit=True, no_exec=False, label=True): # Modes for which we can't unlock if mode == HSM2Dongle.MODE.UNKNOWN: - raise AdminError("Device mode unknown. Already unlocked? Otherwise disconnect " - "and re-connect the ledger and try again") + raise AdminError("Device mode unknown. Already unlocked? Otherwise " + f"{Platform.message("restart")} and try again") if mode == HSM2Dongle.MODE.SIGNER or mode == HSM2Dongle.MODE.UI_HEARTBEAT: raise AdminError("Device already unlocked") @@ -87,9 +88,10 @@ def do_unlock(options, exit=True, no_exec=False, label=True): raise AdminError("Unable to unlock: PIN mismatch") info("PIN accepted") + # **** Ledger only **** # Exit the bootloader, go into menu (or, if app is properly signed, into # the app) - if exit: + if Platform.is_ledger() and exit: autoexec = not (options.no_exec or no_exec) info(f"Exiting to menu/app (execute signer: {bls(autoexec)})... ", options.verbose) diff --git a/middleware/build/adm_sgx b/middleware/build/adm_sgx new file mode 100755 index 00000000..e8deba1b --- /dev/null +++ b/middleware/build/adm_sgx @@ -0,0 +1,2 @@ +#!/bin/bash +source $(dirname $0)/bld-docker adm_sgx diff --git a/middleware/build/all b/middleware/build/all index b54b0f87..baeb373d 100755 --- a/middleware/build/all +++ b/middleware/build/all @@ -6,8 +6,10 @@ BINDIR=$(realpath $BUILDDIR/../bin/) echo "Building all..." QUIET=1 $BUILDDIR/manager_ledger && \ +QUIET=1 $BUILDDIR/manager_sgx && \ QUIET=1 $BUILDDIR/manager_tcp && \ QUIET=1 $BUILDDIR/adm_ledger && \ +QUIET=1 $BUILDDIR/adm_sgx && \ QUIET=1 $BUILDDIR/lbutils && \ QUIET=1 $BUILDDIR/signapp && \ echo "" && sha256sum $BINDIR/*.tgz diff --git a/middleware/comm/platform.py b/middleware/comm/platform.py new file mode 100644 index 00000000..b161ac25 --- /dev/null +++ b/middleware/comm/platform.py @@ -0,0 +1,65 @@ +# The MIT License (MIT) +# +# Copyright (c) 2021 RSK Labs Ltd +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +class Platform: + LEDGER = "Ledger" + SGX = "SGX" + X86 = "X86" + VALID_PLATFORMS = [LEDGER, SGX, X86] + + MESSAGES = { + LEDGER: { + "restart": "disconnect and re-connect the ledger nano", + }, + SGX: { + "restart": "restart the SGX powHSM", + }, + X86: { + "restart": "restart the TCPSigner", + } + } + + _platform = None + _options = None + + @classmethod + def set(klass, plf, options={}): + if plf not in klass.VALID_PLATFORMS: + raise RuntimeError("Invalid platform given") + klass._platform = plf + klass._options = options + + @classmethod + def is_ledger(klass): + return klass._platform == Platform.LEDGER + + @classmethod + def is_sgx(klass): + return klass._platform == Platform.SGX + + @classmethod + def options(klass, key): + return klass._options[key] + + @classmethod + def message(klass, key): + return klass.MESSAGES[klass._platform][key] diff --git a/middleware/ledger/protocol.py b/middleware/ledger/protocol.py index ec6976cf..4e6af5cc 100644 --- a/middleware/ledger/protocol.py +++ b/middleware/ledger/protocol.py @@ -22,6 +22,7 @@ import time from comm.protocol import HSM2Protocol, HSM2ProtocolError, HSM2ProtocolInterrupt +from comm.platform import Platform from ledger.hsm2dongle import ( HSM2Dongle, HSM2DongleBaseError, @@ -46,11 +47,6 @@ class HSM2ProtocolLedger(HSM2Protocol): # Required minimum number of pin retries available to proceed with unlocking MIN_AVAILABLE_RETRIES = 2 - # Default user messages - MESSAGES = { - "restart": "restart the powHSM" - } - def __init__(self, pin, dongle): super().__init__() self.hsm2dongle = dongle @@ -78,7 +74,7 @@ def initialize_device(self): self.logger.info( "Could not determine onboarded status. If unlocked, " + "please enter the signing app and rerun the manager. Otherwise," - + f"{self.MESSAGES["restart"]} and try again" + + f"{Platform.message("restart")} and try again" ) raise HSM2ProtocolInterrupt() @@ -201,12 +197,12 @@ def _handle_bootloader(self): raise Exception("Dongle reported fail to change pin. Pin invalid?") self.pin.commit_change() self.logger.info( - f"PIN changed. Please {self.MESSAGES["restart"]}" + f"PIN changed. Please {Platform.message("restart")}" ) except Exception as e: self.pin.abort_change() self.logger.error( - f"Error changing PIN: %s. Please {self.MESSAGES["restart"]} " + f"Error changing PIN: %s. Please {Platform.message("restart")} " "and try again", format(e), ) finally: diff --git a/middleware/manager_ledger.py b/middleware/manager_ledger.py index c88f3602..335cfdcf 100644 --- a/middleware/manager_ledger.py +++ b/middleware/manager_ledger.py @@ -25,6 +25,7 @@ from mgr.runner import ManagerRunner from ledger.pin import FileBasedPin from user.options import UserOptionParser +from comm.platform import Platform def load_pin(user_options): @@ -39,18 +40,13 @@ def load_pin(user_options): return pin -def configure_protocol_messages(protocol): - protocol.MESSAGES = { - "restart": "disconnect and reconnect the ledger nano", - } - - if __name__ == "__main__": + Platform.set(Platform.LEDGER) user_options = UserOptionParser("Start the powHSM manager for Ledger", with_pin=True).parse() runner = ManagerRunner("powHSM manager", lambda options: HSM2Dongle(options.io_debug), - load_pin, configure_protocol_messages) + load_pin) runner.run(user_options) diff --git a/middleware/manager_sgx.py b/middleware/manager_sgx.py index 9b443181..010f16ba 100644 --- a/middleware/manager_sgx.py +++ b/middleware/manager_sgx.py @@ -25,6 +25,7 @@ from mgr.runner import ManagerRunner from ledger.pin import FileBasedPin from user.options import UserOptionParser +from comm.platform import Platform def load_pin(user_options): @@ -39,13 +40,8 @@ def load_pin(user_options): return pin -def configure_protocol_messages(protocol): - protocol.MESSAGES = { - "restart": "restart the SGX powHSM", - } - - if __name__ == "__main__": + Platform.set(Platform.SGX) user_options = UserOptionParser("Start the powHSM manager for SGX", with_pin=True, with_tcpconn=True, @@ -56,6 +52,6 @@ def configure_protocol_messages(protocol): lambda options: HSM2DongleSGX(options.tcpconn_host, options.tcpconn_port, options.io_debug), - load_pin, configure_protocol_messages) + load_pin) runner.run(user_options) diff --git a/middleware/manager_tcp.py b/middleware/manager_tcp.py index de3d73db..11ce1e05 100644 --- a/middleware/manager_tcp.py +++ b/middleware/manager_tcp.py @@ -23,15 +23,11 @@ from ledger.hsm2dongle_tcp import HSM2DongleTCP from mgr.runner import ManagerRunner from user.options import UserOptionParser - - -def configure_protocol_messages(protocol): - protocol.MESSAGES = { - "restart": "restart the TCPSigner", - } +from comm.platform import Platform if __name__ == "__main__": + Platform.set(Platform.X86) user_options = UserOptionParser("Start the powHSM manager for TCPSigner", with_pin=False, with_tcpconn=True, @@ -41,7 +37,6 @@ def configure_protocol_messages(protocol): lambda options: HSM2DongleTCP(options.tcpconn_host, options.tcpconn_port, options.io_debug), - load_pin=lambda options: None, - configure_protocol=configure_protocol_messages) + load_pin=lambda options: None) runner.run(user_options) diff --git a/middleware/mgr/runner.py b/middleware/mgr/runner.py index 95647ca9..df65e535 100644 --- a/middleware/mgr/runner.py +++ b/middleware/mgr/runner.py @@ -29,11 +29,10 @@ class ManagerRunner: - def __init__(self, name, create_dongle, load_pin, configure_protocol=None): + def __init__(self, name, create_dongle, load_pin): self.name = name self.create_dongle = create_dongle self.load_pin = load_pin - self.configure_protocol = configure_protocol def run(self, user_options): configure_logging(user_options.logconfigfilepath) @@ -52,8 +51,6 @@ def run(self, user_options): else: logger.info("Using protocol version 2") protocol = HSM2ProtocolLedger(pin, dongle) - if self.configure_protocol: - self.configure_protocol(protocol) server = TCPServer(user_options.host, user_options.port, protocol) server.run() except PinError as e: diff --git a/middleware/sgx/hsm2dongle.py b/middleware/sgx/hsm2dongle.py index 4a463907..8e2d8bb8 100644 --- a/middleware/sgx/hsm2dongle.py +++ b/middleware/sgx/hsm2dongle.py @@ -21,10 +21,12 @@ # SOFTWARE. from enum import IntEnum +from ledger.hsm2dongle import HSM2DongleError from ledger.hsm2dongle_tcp import HSM2DongleTCP class SgxCommand(IntEnum): + SGX_ONBOARD = 0xA0, SGX_RETRIES = 0xA2, SGX_UNLOCK = 0xA3, SGX_ECHO = 0xA4, @@ -58,3 +60,19 @@ def new_pin(self, pin): def get_retries(self): apdu_rcv = self._send_command(SgxCommand.SGX_RETRIES) return apdu_rcv[2] + + # Attempt to onboard the device using the given seed and pin + def onboard(self, seed, pin): + if type(seed) != bytes or len(seed) != self.ONBOARDING.SEED_LENGTH: + raise HSM2DongleError("Invalid seed given") + + if type(pin) != bytes: + raise HSM2DongleError("Invalid pin given") + + self.logger.info("Sending onboard command") + response = self._send_command(SgxCommand.SGX_ONBOARD, bytes([0x0]) + seed + pin) + + if response[2] != 1: + raise HSM2DongleError("Error onboarding. Got '%s'" % response.hex()) + + return True diff --git a/middleware/tests/admin/test_adm_sgx.py b/middleware/tests/admin/test_adm_sgx.py new file mode 100644 index 00000000..d5a5f5cb --- /dev/null +++ b/middleware/tests/admin/test_adm_sgx.py @@ -0,0 +1,171 @@ +# The MIT License (MIT) +# +# Copyright (c) 2021 RSK Labs Ltd +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys +from argparse import Namespace +from unittest import TestCase +from unittest.mock import call, patch +from adm_sgx import main +import logging + +logging.disable(logging.CRITICAL) + + +class TestAdmLedger(TestCase): + def setUp(self): + self.old_stdout = sys.stdout + self.DEFAULT_OPTIONS = { + "sgx_host": "localhost", + "sgx_port": 7777, + "any_pin": False, + "new_pin": None, + "no_unlock": False, + "operation": None, + "output_file_path": None, + "pin": None, + "verbose": False, + } + + def tearDown(self): + sys.stdout = self.old_stdout + + @patch("adm_sgx.do_unlock") + def test_unlock(self, do_unlock): + expected_options = { + **self.DEFAULT_OPTIONS, + 'operation': 'unlock', + 'pin': 'a-pin', + } + expected_call_args_list = [ + call(Namespace(**expected_options)), + call(Namespace(**expected_options)) + ] + + with patch('sys.argv', ['adm_sgx.py', '-p', 'a-pin', 'unlock']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + with patch('sys.argv', ['adm_sgx.py', '--pin', 'a-pin', 'unlock']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + self.assertTrue(do_unlock.called) + self.assertEqual(do_unlock.call_count, 2) + self.assertEqual(expected_call_args_list, do_unlock.call_args_list) + + @patch("adm_sgx.do_onboard") + def test_onboard(self, do_onboard): + expected_options = { + **self.DEFAULT_OPTIONS, + 'operation': 'onboard', + 'pin': 'a-pin', + } + + expected_call_args_list = [ + call(Namespace(**expected_options)), + call(Namespace(**expected_options)) + ] + + with patch('sys.argv', + ['adm_sgx.py', '-p', 'a-pin', 'onboard']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + with patch('sys.argv', + ['adm_sgx.py', '--pin', 'a-pin', 'onboard']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + self.assertTrue(do_onboard.called) + self.assertEqual(expected_call_args_list, do_onboard.call_args_list) + + @patch("adm_sgx.do_get_pubkeys") + def test_pubkeys(self, do_get_pubkeys): + expected_options = { + **self.DEFAULT_OPTIONS, + 'no_unlock': True, + 'operation': 'pubkeys', + 'output_file_path': 'a-path', + 'pin': 'a-pin', + 'sgx_host': '1.2.3.4', + } + + expected_call_args_list = [ + call(Namespace(**expected_options)), + call(Namespace(**expected_options)) + ] + + with patch('sys.argv', ['adm_sgx.py', '-p', 'a-pin', '-o', 'a-path', '-u', + '-s', '1.2.3.4', 'pubkeys']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + with patch('sys.argv', + ['adm_sgx.py', + '--host', '1.2.3.4', + '--pin', 'a-pin', + '--output', 'a-path', + '--nounlock', + 'pubkeys']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + self.assertTrue(do_get_pubkeys.called) + self.assertEqual(expected_call_args_list, do_get_pubkeys.call_args_list) + + @patch("adm_sgx.do_changepin") + def test_changepin(self, do_changepin): + expected_options = { + **self.DEFAULT_OPTIONS, + 'any_pin': True, + 'new_pin': 'new-pin', + 'operation': 'changepin', + 'pin': 'old-pin', + 'sgx_port': 4567, + } + expected_call_args_list = [ + call(Namespace(**expected_options)), + call(Namespace(**expected_options)) + ] + + with patch('sys.argv', ['adm_sgx.py', '-p', 'old-pin', '-n', 'new-pin', + '-r', '4567', '-a', 'changepin']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + with patch('sys.argv', ['adm_sgx.py', + '--newpin', 'new-pin', '--anypin', 'changepin', + '--port', '4567', '--pin', 'old-pin']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + self.assertTrue(do_changepin.called) + self.assertEqual(do_changepin.call_count, 2) + self.assertEqual(expected_call_args_list, do_changepin.call_args_list) diff --git a/middleware/tests/admin/test_changepin.py b/middleware/tests/admin/test_changepin.py index 033e5592..1d5dc24d 100644 --- a/middleware/tests/admin/test_changepin.py +++ b/middleware/tests/admin/test_changepin.py @@ -25,6 +25,7 @@ from unittest.mock import Mock, call, patch from admin.changepin import do_changepin from admin.misc import AdminError +from comm.platform import Platform from ledger.hsm2dongle import HSM2Dongle import logging @@ -48,6 +49,7 @@ def setUp(self): } self.default_options = SimpleNamespace(**options) self.dongle = Mock() + Platform.set(Platform.LEDGER) @patch("admin.changepin.do_unlock") def test_changepin(self, do_unlock_mock, get_hsm, _): @@ -77,7 +79,7 @@ def test_changepin_unlock_error(self, do_unlock_mock, get_hsm, _): self.assertEqual('Failed to unlock device: unlock-error', str(e.exception)) @patch("admin.changepin.do_unlock") - def test_changepin_invalid_mode(self, do_unlock_mock, get_hsm, _): + def test_changepin_invalid_mode_ledger(self, do_unlock_mock, get_hsm, _): get_hsm.return_value = self.dongle self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER) self.dongle.is_onboarded = Mock(return_value=True) @@ -90,6 +92,21 @@ def test_changepin_invalid_mode(self, do_unlock_mock, get_hsm, _): self.assertTrue(str(e.exception).startswith('Device not in bootloader mode.')) self.assertFalse(self.dongle.new_pin.called) + @patch("admin.changepin.do_unlock") + def test_changepin_signer_mode_sgx(self, do_unlock_mock, get_hsm, _): + Platform.set(Platform.SGX) + get_hsm.return_value = self.dongle + self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER) + self.dongle.is_onboarded = Mock(return_value=True) + self.dongle.new_pin = Mock(return_value=True) + + do_changepin(self.default_options) + + self.assertTrue(do_unlock_mock.called) + self.assertTrue(self.dongle.new_pin.called) + self.assertEqual([call(self.VALID_PIN.encode())], + self.dongle.new_pin.call_args_list) + def test_changepin_invalid_pin(self, get_hsm, _): get_hsm.return_value = self.dongle diff --git a/middleware/tests/admin/test_onboard.py b/middleware/tests/admin/test_onboard.py index 0f9abd23..c7ab698b 100644 --- a/middleware/tests/admin/test_onboard.py +++ b/middleware/tests/admin/test_onboard.py @@ -25,6 +25,7 @@ from unittest.mock import Mock, call, patch, mock_open from admin.certificate import HSMCertificate, HSMCertificateElement from admin.misc import AdminError +from comm.platform import Platform from admin.onboard import do_onboard import json from ledger.hsm2dongle import HSM2Dongle, HSM2DongleError @@ -62,6 +63,7 @@ class TestOnboard(TestCase): } def setUp(self): + Platform.set(Platform.LEDGER) self.certificate_path = "cert-path" options = { "pin": self.VALID_PIN, @@ -95,8 +97,8 @@ def setUp(self): @patch("admin.onboard.get_admin_hsm") @patch("admin.unlock.get_hsm") @patch("sys.stdin.readline") - def test_onboard(self, readline, get_hsm_unlock, get_admin_hsm, - get_hsm_onboard, info_mock, *_): + def test_onboard_ledger(self, readline, get_hsm_unlock, get_admin_hsm, + get_hsm_onboard, info_mock, *_): get_hsm_onboard.return_value = self.dongle get_hsm_unlock.return_value = self.dongle get_admin_hsm.return_value = self.dongle @@ -125,6 +127,32 @@ def test_onboard(self, readline, get_hsm_unlock, get_admin_hsm, self.assertTrue(self.dongle.onboard.called) self.assertTrue(self.dongle.handshake.called) + @patch("admin.onboard.get_admin_hsm") + @patch("admin.unlock.get_hsm") + @patch("sys.stdin.readline") + def test_onboard_sgx(self, readline, get_hsm_unlock, get_admin_hsm, + get_hsm_onboard, info_mock, *_): + Platform.set(Platform.SGX) + + get_hsm_onboard.return_value = self.dongle + get_hsm_unlock.return_value = self.dongle + + self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.BOOTLOADER) + self.dongle.is_onboarded = Mock(side_effect=[False, True]) + self.dongle.onboard = Mock() + readline.return_value = "yes\n" + + with patch("builtins.open", mock_open()): + do_onboard(self.default_options) + + self.assertEqual(info_mock.call_args_list[5][0][0], "Onboarded: No") + self.assertEqual(info_mock.call_args_list[10][0][0], "Onboarded") + + self.assertTrue(self.dongle.onboard.called) + self.assertFalse(self.dongle.get_device_key.called) + self.assertFalse(self.dongle.setup_endorsement_key.called) + self.assertFalse(self.dongle.handshake.called) + @patch("admin.onboard.get_admin_hsm") @patch("admin.unlock.get_hsm") @patch("sys.stdin.readline") @@ -280,7 +308,7 @@ def test_onboard_user_cancelled(self, readline, hsm_unlock, hsm_admin, self.assertFalse(file_mock.return_value.write.called) @patch("sys.stdin.readline") - def test_onboard_no_output_file(self, readline, get_hsm, *_): + def test_onboard_no_output_file_ledger(self, readline, get_hsm, *_): readline.return_value = "yes\n" get_hsm.return_value = self.dongle @@ -296,6 +324,23 @@ def test_onboard_no_output_file(self, readline, get_hsm, *_): self.assertEqual("No output file path given", str(e.exception)) self.assertFalse(self.dongle.onboard.called) + @patch("sys.stdin.readline") + def test_onboard_no_output_file_sgx(self, readline, get_hsm, *_): + Platform.set(Platform.SGX) + readline.return_value = "yes\n" + get_hsm.return_value = self.dongle + + self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.BOOTLOADER) + self.dongle.is_onboarded = Mock(return_value=False) + self.dongle.onboard = Mock() + + options = self.default_options + options.output_file_path = None + + do_onboard(options) + + self.assertTrue(self.dongle.onboard.called) + def test_onboard_invalid_pin(self, *_): options = self.default_options options.pin = self.INVALID_PIN diff --git a/middleware/tests/admin/test_unlock.py b/middleware/tests/admin/test_unlock.py index f90cfa31..153500cf 100644 --- a/middleware/tests/admin/test_unlock.py +++ b/middleware/tests/admin/test_unlock.py @@ -24,6 +24,7 @@ from unittest import TestCase from unittest.mock import Mock, call, patch from admin.misc import AdminError +from comm.platform import Platform from admin.unlock import do_unlock from ledger.hsm2dongle import HSM2Dongle from parameterized import parameterized @@ -37,6 +38,7 @@ @patch("admin.unlock.get_hsm") class TestUnlock(TestCase): def setUp(self): + Platform.set(Platform.LEDGER) self.valid_pin = '1234ABCD' self.invalid_pin = '123456789'