From aa6489abaf431a89516205da90b9000acafb119a Mon Sep 17 00:00:00 2001 From: Italo Sampaio <100376888+italo-sampaio@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:48:04 -0300 Subject: [PATCH 01/21] SGX reproducible builds (#219) - Updates SGX Docker base image to `2024.10.2391` - Removes unnecessary dependencies from SGX Dockerfile - Freezes open-enclave version to `0.19.4` - Ensures deterministic build order --- docker/sgx/Dockerfile | 16 ++-------------- firmware/src/sgx/Makefile | 10 +++++----- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/docker/sgx/Dockerfile b/docker/sgx/Dockerfile index 881f8bd7..57785c5f 100644 --- a/docker/sgx/Dockerfile +++ b/docker/sgx/Dockerfile @@ -1,23 +1,11 @@ -FROM openenclavedockerregistry.azurecr.io/oetools-20.04:2023.11.21100 +FROM openenclavedockerregistry.azurecr.io/oetools-20.04:2024.10.2391 # Install dependencies RUN apt-get update && \ apt-get install -y apt-utils vim && \ apt-get install -y tar && \ - apt-get install -y xz-utils && \ apt-get install -y curl && \ - apt-get install -y git && \ - apt-get install -y clang-11 && \ - apt-get install -y libssl-dev && \ - apt-get install -y gdb && \ - apt-get install -y libsgx-enclave-common && \ - apt-get install -y libsgx-quote-ex && \ - apt-get install -y libprotobuf17 && \ - apt-get install -y libsgx-dcap-ql && \ - apt-get install -y libsgx-dcap-ql-dev && \ - apt-get install -y az-dcap-client && \ - apt-get install -y open-enclave && \ - apt-get install -y gcc && \ + apt-get install -y open-enclave=0.19.4 && \ apt-get install -y make # Create directory to host symlinks to Open Enclave static libraries diff --git a/firmware/src/sgx/Makefile b/firmware/src/sgx/Makefile index ba906f93..5e7db387 100644 --- a/firmware/src/sgx/Makefile +++ b/firmware/src/sgx/Makefile @@ -38,13 +38,13 @@ POWHSM_SRC_DIR = ../powhsm/src COMMON_SRC_DIR = ../common/src ## Untrusted source files -UNTRUSTED_SRC = $(wildcard $(SGX_UNTRUSTED_SRC_DIR)/*.c) +UNTRUSTED_SRC = $(sort $(wildcard $(SGX_UNTRUSTED_SRC_DIR)/*.c)) ## Trusted source files -TRUSTED_SRC = $(wildcard $(SGX_TRUSTED_SRC_DIR)/*.c) -TRUSTED_SRC += $(wildcard $(HAL_TRUSTED_SRC_DIR)/*.c) -TRUSTED_SRC += $(wildcard $(POWHSM_SRC_DIR)/*.c) -TRUSTED_SRC += $(wildcard $(COMMON_SRC_DIR)/*.c) +TRUSTED_SRC = $(sort $(wildcard $(SGX_TRUSTED_SRC_DIR)/*.c)) +TRUSTED_SRC += $(sort $(wildcard $(HAL_TRUSTED_SRC_DIR)/*.c)) +TRUSTED_SRC += $(sort $(wildcard $(POWHSM_SRC_DIR)/*.c)) +TRUSTED_SRC += $(sort $(wildcard $(COMMON_SRC_DIR)/*.c)) # Enclave definition files EDL_FILE = $(SGX_SRC_DIR)/$(ENCLAVE_NAME).edl From 2e8d75f1206218150c1ae03be4439eb7081ede0f Mon Sep 17 00:00:00 2001 From: Italo Sampaio <100376888+italo-sampaio@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:22:40 -0300 Subject: [PATCH 02/21] Added script to extract the digest and mrenclave from a signed enclave binary (#221) --- firmware/build/extract-mrenclave | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100755 firmware/build/extract-mrenclave diff --git a/firmware/build/extract-mrenclave b/firmware/build/extract-mrenclave new file mode 100755 index 00000000..68b18fde --- /dev/null +++ b/firmware/build/extract-mrenclave @@ -0,0 +1,67 @@ +#! /usr/bin/env bash + +function print_usage() { + echo "Usage: $0 " + echo " or $0 " + echo "" + echo "Options:" + echo " signed_enclave: path of a signed enclave binary file (MUST end with '.signed' extension)." + echo " unsigned_enclave: path of an unsigned enclave binary file." + echo " config_file: configuration file specifying the enclave properties." + echo " refer to the oesign sign --help for the list of properties." + echo " this option is only required for unsigned enclaves." + echo "" + echo "Description:" + echo " This script extracts the MRENCLAVE and the DIGEST values from the enclave" + echo " binary and prints them to stdout. The script can be used both for unsigned" + echo " and signed enclave binaries." + echo "" + echo " Signed binaries:" + echo " The MRENCLAVE and DIGEST are calculated from the signed enclave binary." + echo " Both values are printed in hexadecimal format to stdout." + echo "" + echo " Unsigned binaries:" + echo " The DIGEST is calculated from the unsigned enclave binary and the enclave" + echo " properties specified in the configuration file. The MRENCLAVE is set to zero." + echo " Both values are printed in hexadecimal format to stdout." +} + +if [[ $# -lt 1 ]]; then + print_usage + exit 1 +fi + +pushd $(dirname $0) > /dev/null +BUILD_ROOT=$(pwd) +popd > /dev/null + +HSM_ROOT=$(realpath $BUILD_ROOT/../../) + +DOCKER_IMAGE=hsm:sgx +source $BUILD_ROOT/../../docker/check-image + +ENCLAVE_BIN=$(realpath $1 --relative-to=$HSM_ROOT) +if [[ ! -f $ENCLAVE_BIN ]]; then + echo "Invalid enclave path: $ENCLAVE_BIN" + exit 1 +else + ENCLAVE_ARG="-e $ENCLAVE_BIN" +fi + +if [[ $ENCLAVE_BIN == *.signed ]]; then + CONFIG_ARG="" +elif [[ $# -ge 2 ]]; then + CONFIG_ARG="-c $(realpath $2 --relative-to=$HSM_ROOT)" +else + echo "Invalid usage" + print_usage + exit 1 +fi + +DIGEST_CMD="oesign digest $ENCLAVE_ARG $CONFIG_ARG -d /tmp/enclave_digest > /dev/null && hexdump -v -e '/1 \"%02x\"' /tmp/enclave_digest" +MRENCLAVE_CMD="oesign dump $ENCLAVE_ARG | grep mrenclave | cut -d '=' -f 2" +EXTRACT_CMD="\$SGX_ENVSETUP && echo digest: \$($DIGEST_CMD) && echo mrenclave: \$($MRENCLAVE_CMD)" + +DOCKER_USER="$(id -u):$(id -g)" + +docker run -t --rm --user $DOCKER_USER -w /hsm2 -v ${HSM_ROOT}:/hsm2 ${DOCKER_IMAGE} /bin/bash -c "$EXTRACT_CMD" From 738fb0fad9753da2483c1cb35382fa429f8a6abf Mon Sep 17 00:00:00 2001 From: Italo Sampaio <100376888+italo-sampaio@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:36:28 -0300 Subject: [PATCH 03/21] Adds MRENCLAVE and digest information to SGX build scripts (#223) --- build-dist-sgx | 8 ++++++++ firmware/build/build-sgx | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/build-dist-sgx b/build-dist-sgx index 31052197..a450cbe6 100755 --- a/build-dist-sgx +++ b/build-dist-sgx @@ -52,6 +52,14 @@ $ROOT_DIR/firmware/build/build-sgx $CHECKPOINT $DIFFICULTY $NETWORK > /dev/null cp $ROOT_DIR/firmware/src/sgx/bin/hsmsgx $HSM_DIR/ cp $ROOT_DIR/firmware/src/sgx/bin/hsmsgx_enclave.signed $HSM_DIR/ +HOST_HASH=$(sha256sum $ROOT_DIR/firmware/src/sgx/bin/hsmsgx | cut -d ' ' -f 1) +ENCLAVE_HASH=$($ROOT_DIR/firmware/build/extract-mrenclave $ROOT_DIR/firmware/src/sgx/bin/hsmsgx_enclave.signed) +echo "$HSM_DIR/hsmsgx:" +echo $HOST_HASH +echo +echo "$HSM_DIR/hsmsgx_enclave.signed" +echo "$ENCLAVE_HASH" + echo echo -e "\e[32mBuild complete.\e[0m" diff --git a/firmware/build/build-sgx b/firmware/build/build-sgx index cbe70d31..f71c86b1 100755 --- a/firmware/build/build-sgx +++ b/firmware/build/build-sgx @@ -40,3 +40,20 @@ BUILD_CMD="\$SGX_ENVSETUP && make clean $BUILD_TARGET CHECKPOINT=$1 TARGET_DIFFI DOCKER_USER="$(id -u):$(id -g)" docker run -t --rm --user $DOCKER_USER -w /hsm2/firmware/src/sgx -v ${HSM_ROOT}:/hsm2 ${DOCKER_IMAGE} /bin/bash -c "$BUILD_CMD" + +if [[ $? -ne 0 ]]; then + echo "Build failed" + exit 1 +fi + +HOST_BIN=$HSM_ROOT/firmware/src/sgx/bin/hsmsgx +ENCLAVE_BIN=$HSM_ROOT/firmware/src/sgx/bin/hsmsgx_enclave.signed + +echo "*******************" +echo "Build successful." +echo "$(realpath $HOST_BIN --relative-to=$HSM_ROOT):" +sha256sum $HOST_BIN | cut -d ' ' -f 1 +echo "" +echo "$(realpath $ENCLAVE_BIN --relative-to=$HSM_ROOT):" +$BUILD_ROOT/extract-mrenclave $ENCLAVE_BIN +echo "*******************" From 1d0044fa2843195cd22125224361eb60a7f9c70d Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Tue, 26 Nov 2024 02:40:49 +1300 Subject: [PATCH 04/21] Enhancing existing attestation scheme with additional information (#222) - Implemented new attestation protocol in firmware - Moved attestation context definition to attestation header file - Updated admin tooling to gather and validate new attestation format, keeping support for legacy format - Factored out attestation gathering logic from HSM2Dongle - New semantics for code hash and public key gathering functions in existing endorsement module - Additional endorsement module functions to allow for envelope gathering - New platform module to provide platform id and timestamp - Added and updated unit tests - Updated attestation documentation - Added SGX endorsement library's envelope function stubs --- docs/attestation.md | 47 +++-- firmware/src/hal/include/hal/endorsement.h | 18 +- firmware/src/hal/include/hal/platform.h | 13 ++ firmware/src/hal/ledger/src/endorsement.c | 23 +++ firmware/src/hal/ledger/src/platform.c | 8 + .../src/hal/sgx/src/trusted/endorsement.c | 10 + firmware/src/hal/x86/src/endorsement.c | 8 + firmware/src/hal/x86/src/platform.c | 11 +- firmware/src/ledger/signer/src/main.c | 2 + firmware/src/powhsm/src/attestation.c | 168 ++++++++++++++--- firmware/src/powhsm/src/attestation.h | 46 ++++- firmware/src/powhsm/src/mem.h | 13 +- middleware/adm_ledger.py | 4 +- .../{attestation.py => ledger_attestation.py} | 13 +- ...tation.py => verify_ledger_attestation.py} | 85 ++++++--- middleware/ledger/hsm2dongle.py | 40 +--- middleware/ledger/hsm2dongle_cmds/__init__.py | 1 + middleware/ledger/hsm2dongle_cmds/command.py | 2 +- .../hsm2dongle_cmds/powhsm_attestation.py | 81 ++++++++ ...estation.py => test_ledger_attestation.py} | 41 ++-- ...n.py => test_verify_ledger_attestation.py} | 177 ++++++++++++------ .../test_powhsm_attestation.py | 121 ++++++++++++ 22 files changed, 730 insertions(+), 202 deletions(-) rename middleware/admin/{attestation.py => ledger_attestation.py} (90%) rename middleware/admin/{verify_attestation.py => verify_ledger_attestation.py} (75%) create mode 100644 middleware/ledger/hsm2dongle_cmds/powhsm_attestation.py rename middleware/tests/admin/{test_attestation.py => test_ledger_attestation.py} (89%) rename middleware/tests/admin/{test_verify_attestation.py => test_verify_ledger_attestation.py} (68%) create mode 100644 middleware/tests/ledger/hsm2dongle_cmds/test_powhsm_attestation.py diff --git a/docs/attestation.md b/docs/attestation.md index bba1a8bc..530aa713 100644 --- a/docs/attestation.md +++ b/docs/attestation.md @@ -66,7 +66,17 @@ As a consequence of the aforementioned features, this message guarantees that th ### Signer attestation -To generate the attestation, the Signer uses the configured attestation scheme to sign a message containing a predefined header (`HSM:SIGNER:5.3`) and the `sha256sum` of the concatenation of the authorized public keys (see the [protocol](./protocol.md) for details on this) lexicographically ordered by their UTF-encoded derivation path. This message guarantees that the device is running a specific version of the Signer and that those keys are in control of the ledger device. +To generate the attestation, the Signer uses the configured attestation scheme to sign a message generated by the concatenation of: + +- A predefined header (`POWHSM:5.4::`). +- A 3-byte platform identifier, which for Ledger is exactly the ASCII characters `led`. +- A 32 byte user-defined value. By default, the attestation generation client supplies the latest RSK block hash as this value, so it can then be used as a minimum timestamp reference for the attestation generation. +- A 32 byte value that is generated by computing the `sha256sum` of the concatenation of the authorized public keys (see the [protocol](./protocol.md) for details on this) lexicographically ordered by their UTF-encoded derivation path. +- A 32 byte value denoting the device's current known best block hash for the Rootstock network. +- An 8 byte value denoting the leading bytes of the latest authorised signed Bitcoin transaction hash. +- An 8 byte value denoting a big-endian unix timestamp. For Ledger, this is always zero. + +This message guarantees that the device is running a specific version of the Signer and that those keys are in control of the ledger device. The additional fields aid in auditing a device's state at the time the attestation is gathered (e.g., for firmware updates). ## Attestation file format @@ -101,7 +111,7 @@ The output of the attestation process is a JSON file with a proprietary structur }, { "name": "signer", - "message": "48534d3a5349474e45523a332e30a2316e4c4e07e77ae65c74574452f330ed62752ba4c66f9c2101836d7b36cef2", + "message": "504f5748534d3a352e343a3a6c656413c3581aa97c8169d3994e9369c11ebd63bcf123d0671634f21b568983d3291687fd9b1f4aa83e348906e2efd6cbed98e39d17aea4c03d73f30e99d602d67633bdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea4209b659a04529d6811dd0000000000000000", "signature": "30440220154bb544fe00df5635c03618ee9614d50933fe7c9226d8efce55f1a40832681402206289dab7b8d6700e048b602ac03516e0e6a1609796fc27c440848d072af71c2a", "signed_by": "attestation", "tweak": "e1baa18564fc0c2c70ac4019609c6db643adbf12711c8b319f838e6a74b0da2c" @@ -158,23 +168,30 @@ to then obtain the following sample output: Using 0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f818057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609 as root authority -------------------------------------------------------------------------------------------------------- UI verified with: -UD value: c4207b260c5b6964190568e528ec0b212a70e512ed6bdcef5e192362852a3839 -Derived public key (m/44'/0'/0'/0/0): 03198eb60255fefc3478d0a78c11f5124c938f66fdaa62f9e9c543c6ced031ef37 -Authorized signer hash: e1baa18564fc0c2c70ac4019609c6db643adbf12711c8b319f838e6a74b0da2c +UD value: 13c3581aa97c8169d3994e9369c11ebd63bcf123d0671634f21b568983d32916 +Derived public key (m/44'/0'/0'/0/0): 0254464d36eaa08a2c31a80eb902e7400563f403c85ef51dd73aaadb57967b61e8 +Authorized signer hash: cc3c55563a4fa50d973faf704d7ef4f272b99ed7e0e0848457dd60be7d3df4b5 Authorized signer iteration: 1 -Installed UI hash: 17f2129265b071e3d8658a549cd60720c86e34c7a6b81d517ffef123c8425f19 +Installed UI hash: 7674c4870ff06ace61d468df8af521be6cc40e86ca6a6b732453801e6b7adf9d +Installed UI version: 5.4 -------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------- Signer verified with public keys: -m/44'/0'/0'/0/0: 03198eb60255fefc3478d0a78c11f5124c938f66fdaa62f9e9c543c6ced031ef37 -m/44'/1'/0'/0/0: 0309fe4c9a803658c1d1c0c19f2d841e34306d172f0bb092431ace7bbda334e902 -m/44'/1'/1'/0/0: 023ac8c77507fdcb7581ce3ee366a7b09791b54377af67f75e1a159737f4f77fe7 -m/44'/1'/2'/0/0: 02583d0dec06114cc0a19883398652d8f87af0175f7d7c2c97417622341e06560c -m/44'/137'/0'/0/0: 03458e7f8f7885f0b0648a8e2e899fe838a7f93da0028634689438e460d3ba614f -m/44'/137'/1'/0/0: 03e27a65c9e6ff0d3fc4085aa84f8d7ec467edf6ae6b30ed40d96d4344b516f4c6 - -Hash: a2316e4c4e07e77ae65c74574452f330ed62752ba4c66f9c2101836d7b36cef2 -Installed Signer hash: e1baa18564fc0c2c70ac4019609c6db643adbf12711c8b319f838e6a74b0da2c +m/44'/0'/0'/0/0: 0254464d36eaa08a2c31a80eb902e7400563f403c85ef51dd73aaadb57967b61e8 +m/44'/1'/0'/0/0: 02a7171ba5fcdf9ae8a32b733cbe748b6007b4633939ba1c8baca074e9358a281a +m/44'/1'/1'/0/0: 022e777db5856568da55947c1a60df4ec28b8fb27ea182de54575b3aadc4559932 +m/44'/1'/2'/0/0: 0307455520c1b365436741c98ddc987c8ed7adddf67b8b69e5763f930c0131727e +m/44'/137'/0'/0/0: 02ecdf31ca81e7c5a2949dad38536676eee2647ec2e41c0771cd4e918b5c2fc4f8 +m/44'/137'/1'/0/0: 0345ac500d260c1f6794b21fad8acce66548fee7a463befd5a0ec5bb73b9ae4df1 +Hash: 72237ee55064aebd5ab13d179c61bfb41c5b1d2ed7e018f8de46a7262c8cf1ec + +Installed Signer hash: cc3c55563a4fa50d973faf704d7ef4f272b99ed7e0e0848457dd60be7d3df4b5 +Installed Signer version: 5.4 +Platform: led +UD value: 13c3581aa97c8169d3994e9369c11ebd63bcf123d0671634f21b568983d32916 +Best block: bdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea4209b +Last transaction signed: 659a04529d6811dd +Timestamp: 0000000000000000 --------------------------------------------------------------------------------------- ``` diff --git a/firmware/src/hal/include/hal/endorsement.h b/firmware/src/hal/include/hal/endorsement.h index 8b43ccde..1e508e55 100644 --- a/firmware/src/hal/include/hal/endorsement.h +++ b/firmware/src/hal/include/hal/endorsement.h @@ -45,6 +45,22 @@ bool endorsement_sign(uint8_t* msg, uint8_t* signature_out, uint8_t* signature_out_length); +/** + * @brief Gets a pointer to the last signed envelope + * + * @return a pointer to a buffer containing the envelope, + * or NULL if no envelope is available. + */ +uint8_t* endorsement_get_envelope(); + +/** + * @brief Gets the length of the last signed envelope + * + * @return the byte length of the last signed envelope, + * or ZERO if no envelope is available. + */ +size_t endorsement_get_envelope_length(); + /** * @brief Grabs the hash of the currently running code * @@ -99,7 +115,7 @@ extern attestation_id_t attestation_id; */ bool endorsement_init(char* att_file_path); -#elif defined(HSM_PLATFORM_SGX) +#elif defined(HSM_PLATFORM_SGX) || defined(HSM_PLATFORM_LEDGER) /** * @brief Initializes the endorsement module diff --git a/firmware/src/hal/include/hal/platform.h b/firmware/src/hal/include/hal/platform.h index 3b0af3ae..4bca6cf6 100644 --- a/firmware/src/hal/include/hal/platform.h +++ b/firmware/src/hal/include/hal/platform.h @@ -28,6 +28,9 @@ #include #include +// Size in bytes of a platform id +#define PLATFORM_ID_LENGTH 3 + /** * @brief Perform the platform-specific version of memmove * @@ -42,6 +45,16 @@ void platform_memmove(void *dst, const void *src, unsigned int length); */ void platform_request_exit(); +/** + * @brief Get the current platform id + */ +const char *platform_get_id(); + +/** + * @brief Get the current timestamp + */ +uint64_t platform_get_timestamp(); + /** * X86 specific headers */ diff --git a/firmware/src/hal/ledger/src/endorsement.c b/firmware/src/hal/ledger/src/endorsement.c index dd6708e3..1b2e60aa 100644 --- a/firmware/src/hal/ledger/src/endorsement.c +++ b/firmware/src/hal/ledger/src/endorsement.c @@ -29,6 +29,21 @@ // Index of the ledger endorsement scheme #define ENDORSEMENT_SCHEME_INDEX 2 +static bool sign_performed; + +bool endorsement_init() { + sign_performed = false; + return true; +} + +uint8_t* endorsement_get_envelope() { + return NULL; +} + +size_t endorsement_get_envelope_length() { + return 0; +} + bool endorsement_sign(uint8_t* msg, size_t msg_size, uint8_t* signature_out, @@ -41,11 +56,15 @@ bool endorsement_sign(uint8_t* msg, *signature_out_length = os_endorsement_key2_derive_sign_data(msg, msg_size, signature_out); + sign_performed = true; return true; } bool endorsement_get_code_hash(uint8_t* code_hash_out, uint8_t* code_hash_out_length) { + if (!sign_performed) { + return false; + } if (*code_hash_out_length < HASH_LENGTH) { return false; @@ -57,6 +76,10 @@ bool endorsement_get_code_hash(uint8_t* code_hash_out, bool endorsement_get_public_key(uint8_t* public_key_out, uint8_t* public_key_out_length) { + if (!sign_performed) { + return false; + } + if (*public_key_out_length < PUBKEY_UNCMP_LENGTH) { return false; } diff --git a/firmware/src/hal/ledger/src/platform.c b/firmware/src/hal/ledger/src/platform.c index ecea2a79..fa6dea58 100644 --- a/firmware/src/hal/ledger/src/platform.c +++ b/firmware/src/hal/ledger/src/platform.c @@ -38,4 +38,12 @@ void platform_request_exit() { } } END_TRY_L(exit); +} + +const char *platform_get_id() { + return "led"; +} + +uint64_t platform_get_timestamp() { + return (uint64_t)0; } \ No newline at end of file diff --git a/firmware/src/hal/sgx/src/trusted/endorsement.c b/firmware/src/hal/sgx/src/trusted/endorsement.c index 4d995e2b..2fd21e99 100644 --- a/firmware/src/hal/sgx/src/trusted/endorsement.c +++ b/firmware/src/hal/sgx/src/trusted/endorsement.c @@ -113,6 +113,16 @@ bool endorsement_init() { return true; } +// TODO: Implement +uint8_t* endorsement_get_envelope() { + return NULL; +} + +// TODO: Implement +size_t endorsement_get_envelope_length() { + return 0; +} + bool endorsement_sign(uint8_t* msg, size_t msg_size, uint8_t* signature_out, diff --git a/firmware/src/hal/x86/src/endorsement.c b/firmware/src/hal/x86/src/endorsement.c index f127660f..42283b24 100644 --- a/firmware/src/hal/x86/src/endorsement.c +++ b/firmware/src/hal/x86/src/endorsement.c @@ -171,6 +171,14 @@ bool endorsement_init(char* att_file_path) { return true; } +uint8_t* endorsement_get_envelope() { + return NULL; +} + +size_t endorsement_get_envelope_length() { + return 0; +} + bool endorsement_sign(uint8_t* msg, size_t msg_size, uint8_t* signature_out, diff --git a/firmware/src/hal/x86/src/platform.c b/firmware/src/hal/x86/src/platform.c index 5cf96a53..e6b6a5e8 100644 --- a/firmware/src/hal/x86/src/platform.c +++ b/firmware/src/hal/x86/src/platform.c @@ -26,6 +26,7 @@ #include "hal/log.h" #include +#include void platform_memmove(void *dst, const void *src, unsigned int length) { memmove(dst, src, length); @@ -34,4 +35,12 @@ void platform_memmove(void *dst, const void *src, unsigned int length) { void platform_request_exit() { // Currently unsupported, just log the call LOG("platform_request_exit called\n"); -} \ No newline at end of file +} + +const char *platform_get_id() { + return "x86"; +} + +uint64_t platform_get_timestamp() { + return (uint64_t)time(NULL); +} diff --git a/firmware/src/ledger/signer/src/main.c b/firmware/src/ledger/signer/src/main.c index 4f4e15ab..b2c6ef09 100644 --- a/firmware/src/ledger/signer/src/main.c +++ b/firmware/src/ledger/signer/src/main.c @@ -47,6 +47,7 @@ // HAL includes #include "hal/communication.h" +#include "hal/endorsement.h" // The interval between two subsequent ticker events in milliseconds. This is // assumed to be 100ms according to the nanos-secure-sdk documentation. @@ -192,6 +193,7 @@ __attribute__((section(".boot"))) int main(int argc, char **argv) { // HAL modules initialization communication_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer)); + endorsement_init(); // HSM context initialization hsm_init(); diff --git a/firmware/src/powhsm/src/attestation.c b/firmware/src/powhsm/src/attestation.c index 06399384..712fbcc8 100644 --- a/firmware/src/powhsm/src/attestation.c +++ b/firmware/src/powhsm/src/attestation.c @@ -33,6 +33,7 @@ #include "apdu.h" #include "defs.h" #include "pathAuth.h" +#include "bc_state.h" #include "bc_hash.h" #include "mem.h" #include "memutil.h" @@ -40,9 +41,27 @@ // Attestation message prefix const char att_msg_prefix[ATT_MSG_PREFIX_LENGTH] = ATT_MSG_PREFIX; -// ----------------------------------------------------------------------- -// Protocol implementation -// ----------------------------------------------------------------------- +// Utility macros for message gathering paging +// Maximum page size is APDU data part size minus one +// byte (first byte of the response), which is used to indicate +// whether there is a next page or not. +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#define PAGESIZE (APDU_TOTAL_DATA_SIZE_OUT - 1) +#define PAGECOUNT(itemcount) (((itemcount) + PAGESIZE - 1) / PAGESIZE) +#define CURPAGESIZE(itemcount, page) (MIN(PAGESIZE, (itemcount) - ((page) * PAGESIZE))) + +static void reset_attestation(att_t* att_ctx) { + explicit_bzero(att_ctx, sizeof(att_t)); + att_ctx->state = STATE_ATTESTATION_WAIT_SIGN; +} + +static void check_state(att_t* att_ctx, + state_attestation_t expected) { + if (att_ctx->state != expected) { + reset_attestation(att_ctx); + THROW(ERR_ATT_PROT_INVALID); + } +} static void hash_public_key(const char* path, size_t path_size, @@ -83,25 +102,61 @@ static void hash_public_key(const char* path, THROW(ERR_ATT_INTERNAL); } +static void write_uint64_be(uint8_t *out, uint64_t in) { + out[0] = (uint8_t)(in >> 56); + out[1] = (uint8_t)(in >> 48); + out[2] = (uint8_t)(in >> 40); + out[3] = (uint8_t)(in >> 32); + out[4] = (uint8_t)(in >> 24); + out[5] = (uint8_t)(in >> 16); + out[6] = (uint8_t)(in >> 8); + out[7] = (uint8_t)in; +} + /* * Generate the attestation message. * * @arg[in] att_ctx attestation context - * @ret generated message size + * @arg[in] ud_value pointer to the user-defined value */ -static unsigned int generate_message_to_sign(att_t* att_ctx) { +static void generate_message_to_sign(att_t* att_ctx, unsigned char* ud_value) { + // Initialize message explicit_bzero(att_ctx->msg, sizeof(att_ctx->msg)); + att_ctx->msg_length = 0; // Copy the message prefix SAFE_MEMMOVE(att_ctx->msg, sizeof(att_ctx->msg), - MEMMOVE_ZERO_OFFSET, + att_ctx->msg_length, (void*)PIC(ATT_MSG_PREFIX), ATT_MSG_PREFIX_LENGTH, MEMMOVE_ZERO_OFFSET, ATT_MSG_PREFIX_LENGTH, THROW(ERR_ATT_INTERNAL)); + att_ctx->msg_length += ATT_MSG_PREFIX_LENGTH; + + // Copy the platform id + SAFE_MEMMOVE(att_ctx->msg, + sizeof(att_ctx->msg), + att_ctx->msg_length, + (void*)PIC(platform_get_id()), + PLATFORM_ID_LENGTH, + MEMMOVE_ZERO_OFFSET, + PLATFORM_ID_LENGTH, + THROW(ERR_ATT_INTERNAL)); + att_ctx->msg_length += PLATFORM_ID_LENGTH; + + // Copy the UD value + SAFE_MEMMOVE(att_ctx->msg, + sizeof(att_ctx->msg), + att_ctx->msg_length, + (void*)PIC(ud_value), + ATT_UD_VALUE_SIZE, + MEMMOVE_ZERO_OFFSET, + ATT_UD_VALUE_SIZE, + THROW(ERR_ATT_INTERNAL)); + att_ctx->msg_length += ATT_UD_VALUE_SIZE; // Prepare the digest SHA256_INIT(&att_ctx->hash_ctx); @@ -111,11 +166,41 @@ static unsigned int generate_message_to_sign(att_t* att_ctx) { hash_public_key(get_ordered_path(i), SINGLE_PATH_SIZE_BYTES, att_ctx); } - SHA256_FINAL(&att_ctx->hash_ctx, &att_ctx->msg[ATT_MSG_PREFIX_LENGTH]); + // Finalise the public keys hash straight into the message + SHA256_FINAL(&att_ctx->hash_ctx, &att_ctx->msg[att_ctx->msg_length]); + att_ctx->msg_length += HASH_LENGTH; - return ATT_MSG_PREFIX_LENGTH + HASH_SIZE; + // Copy the current best block + SAFE_MEMMOVE(att_ctx->msg, + sizeof(att_ctx->msg), + att_ctx->msg_length, + N_bc_state.best_block, + sizeof(N_bc_state.best_block), + MEMMOVE_ZERO_OFFSET, + sizeof(N_bc_state.best_block), + THROW(ERR_ATT_INTERNAL)); + att_ctx->msg_length += sizeof(N_bc_state.best_block); + + // Copy the leading bytes of the last authorised signed tx + SAFE_MEMMOVE(att_ctx->msg, + sizeof(att_ctx->msg), + att_ctx->msg_length, + N_bc_state.last_auth_signed_btc_tx_hash, + sizeof(N_bc_state.last_auth_signed_btc_tx_hash), + MEMMOVE_ZERO_OFFSET, + ATT_LAST_SIGNED_TX_BYTES, + THROW(ERR_ATT_INTERNAL)); + att_ctx->msg_length += ATT_LAST_SIGNED_TX_BYTES; + + // Copy the current timestamp + write_uint64_be(&att_ctx->msg[att_ctx->msg_length], platform_get_timestamp()); + att_ctx->msg_length += sizeof(uint64_t); } +// ----------------------------------------------------------------------- +// Protocol implementation +// ----------------------------------------------------------------------- + /* * Implement the attestation protocol. * @@ -124,44 +209,75 @@ static unsigned int generate_message_to_sign(att_t* att_ctx) { * @ret number of transmited bytes to the host */ unsigned int get_attestation(volatile unsigned int rx, att_t* att_ctx) { - UNUSED(rx); - - unsigned int message_size; - uint8_t code_hash_size; + size_t buf_length; + uint8_t* buf; switch (APDU_OP()) { case OP_ATT_GET: + // Should receive a user-defined value + if (APDU_DATA_SIZE(rx) != ATT_UD_VALUE_SIZE) { + reset_attestation(att_ctx); + THROW(ERR_ATT_PROT_INVALID); + } + // Generate the message to attest - message_size = generate_message_to_sign(att_ctx); + generate_message_to_sign(att_ctx, APDU_DATA_PTR); // Attest message uint8_t endorsement_size = APDU_TOTAL_DATA_SIZE_OUT; if (!endorsement_sign( - att_ctx->msg, message_size, APDU_DATA_PTR, &endorsement_size)) { + att_ctx->msg, att_ctx->msg_length, APDU_DATA_PTR, &endorsement_size)) { THROW(ERR_ATT_INTERNAL); } + // Ready + att_ctx->state = STATE_ATTESTATION_READY; + return TX_FOR_DATA_SIZE(endorsement_size); + case OP_ATT_GET_ENVELOPE: case OP_ATT_GET_MESSAGE: - // Generate and output the message to sign - message_size = generate_message_to_sign(att_ctx); + check_state(att_ctx, STATE_ATTESTATION_READY); + + // Should receive a page index + if (APDU_DATA_SIZE(rx) != 1) + THROW(ERR_ATT_PROT_INVALID); + + // Get the envelope or message + buf = endorsement_get_envelope(); + buf_length = endorsement_get_envelope_length(); + if (!buf || APDU_OP() == OP_ATT_GET_MESSAGE) { + buf = att_ctx->msg; + buf_length = att_ctx->msg_length; + } + + // Check page index within range (page index is zero based) + if (APDU_DATA_PTR[0] >= PAGECOUNT(buf_length)) { + THROW(ERR_ATT_PROT_INVALID); + } + uint8_t page = APDU_DATA_PTR[0]; + // Copy the page into the APDU buffer (no need to check for limits since + // the chunk size is based directly on the APDU size) SAFE_MEMMOVE(APDU_DATA_PTR, - APDU_TOTAL_DATA_SIZE, - MEMMOVE_ZERO_OFFSET, - att_ctx->msg, - sizeof(att_ctx->msg), - MEMMOVE_ZERO_OFFSET, - message_size, + APDU_TOTAL_DATA_SIZE_OUT, + 1, + buf, + buf_length, + APDU_DATA_PTR[0] * PAGESIZE, + CURPAGESIZE(buf_length, page), THROW(ERR_ATT_INTERNAL)); + APDU_DATA_PTR[0] = page < (PAGECOUNT(buf_length) - 1); - return TX_FOR_DATA_SIZE(message_size); + return TX_FOR_DATA_SIZE( + CURPAGESIZE(buf_length, page) + 1); case OP_ATT_APP_HASH: - code_hash_size = APDU_TOTAL_DATA_SIZE_OUT; - if (!endorsement_get_code_hash(APDU_DATA_PTR, &code_hash_size)) { + check_state(att_ctx, STATE_ATTESTATION_READY); + + buf_length = APDU_TOTAL_DATA_SIZE_OUT; + if (!endorsement_get_code_hash(APDU_DATA_PTR, (uint8_t*)&buf_length)) { THROW(ERR_ATT_INTERNAL); } - return TX_FOR_DATA_SIZE(code_hash_size); + return TX_FOR_DATA_SIZE((uint8_t)buf_length); default: THROW(ERR_ATT_PROT_INVALID); break; diff --git a/firmware/src/powhsm/src/attestation.h b/firmware/src/powhsm/src/attestation.h index 5b61be27..7577fda2 100644 --- a/firmware/src/powhsm/src/attestation.h +++ b/firmware/src/powhsm/src/attestation.h @@ -25,17 +25,48 @@ #ifndef __ATTESTATION_H #define __ATTESTATION_H -#include "bc_hash.h" -#include "mem.h" - -// ----------------------------------------------------------------------- -// Keys attestation -// ----------------------------------------------------------------------- +#include "hal/hash.h" // Attestation message prefix -#define ATT_MSG_PREFIX "HSM:SIGNER:5.3" +#define ATT_MSG_PREFIX "POWHSM:5.4::" #define ATT_MSG_PREFIX_LENGTH (sizeof(ATT_MSG_PREFIX) - sizeof("")) +// Attestation UD value size +#define ATT_UD_VALUE_SIZE 32 + +// Number of leading bytes of the last signed BTC tx +// to include in the message +#define ATT_LAST_SIGNED_TX_BYTES 8 + +// Maximum attestation message to sign size +// Prefix: 12 bytes +// Platform: 3 bytes +// UD value: 32 bytes +// Public keys hash: 32 bytes +// Current best block hash: 32 bytes +// Head of latest authorised signed BTC transaction hash: 8 bytes +// Timestamp: 8 bytes +// TOTAL: 127 bytes +#define MAX_ATT_MESSAGE_SIZE 130 + +// Attestation SM states +typedef enum { + STATE_ATTESTATION_WAIT_SIGN = 0, + STATE_ATTESTATION_READY, +} state_attestation_t; + +typedef struct att_s { + state_attestation_t state; + + hash_sha256_ctx_t hash_ctx; // Attestation public keys hashing context + uint8_t msg[MAX_ATT_MESSAGE_SIZE]; // Attestation message + uint8_t msg_length; + + uint32_t path[BIP32_PATH_NUMPARTS]; + uint8_t pubkey[PUBKEY_UNCMP_LENGTH]; + uint8_t pubkey_length; +} att_t; + // ----------------------------------------------------------------------- // Protocol // ----------------------------------------------------------------------- @@ -45,6 +76,7 @@ typedef enum { OP_ATT_GET = 0x01, OP_ATT_GET_MESSAGE = 0x02, OP_ATT_APP_HASH = 0x03, + OP_ATT_GET_ENVELOPE = 0x04, } op_code_attestation_t; // Error codes diff --git a/firmware/src/powhsm/src/mem.h b/firmware/src/powhsm/src/mem.h index ac610f20..58a42f68 100644 --- a/firmware/src/powhsm/src/mem.h +++ b/firmware/src/powhsm/src/mem.h @@ -34,6 +34,7 @@ #include "btctx.h" #include "btcscript.h" #include "auth.h" +#include "attestation.h" #include "heartbeat.h" // ----------------------------------------------------------------------- @@ -41,18 +42,6 @@ // heartbeat. // ----------------------------------------------------------------------- -// Maximum attestation message to sign size (prefix + public keys hash) -#define MAX_ATT_MESSAGE_SIZE 50 - -typedef struct att_s { - hash_sha256_ctx_t hash_ctx; // Attestation public keys hashing context - uint8_t msg[MAX_ATT_MESSAGE_SIZE]; // Attestation message - - uint32_t path[BIP32_PATH_NUMPARTS]; - uint8_t pubkey[PUBKEY_UNCMP_LENGTH]; - uint8_t pubkey_length; -} att_t; - typedef union { struct { block_t block; diff --git a/middleware/adm_ledger.py b/middleware/adm_ledger.py index da484038..758060e6 100644 --- a/middleware/adm_ledger.py +++ b/middleware/adm_ledger.py @@ -30,8 +30,8 @@ from admin.onboard import do_onboard from admin.pubkeys import do_get_pubkeys from admin.changepin import do_changepin -from admin.attestation import do_attestation -from admin.verify_attestation import do_verify_attestation +from admin.ledger_attestation import do_attestation +from admin.verify_ledger_attestation import do_verify_attestation from admin.authorize_signer import do_authorize_signer DEFAULT_ATT_UD_SOURCE = "https://public-node.rsk.co" diff --git a/middleware/admin/attestation.py b/middleware/admin/ledger_attestation.py similarity index 90% rename from middleware/admin/attestation.py rename to middleware/admin/ledger_attestation.py index 2328f1ab..a10fa2e0 100644 --- a/middleware/admin/attestation.py +++ b/middleware/admin/ledger_attestation.py @@ -64,7 +64,7 @@ def do_attestation(options): except RskClientError as e: raise AdminError(f"While fetching the best RSK block hash: {str(e)}") - info(f"Using {ud_value} as the user-defined UI attestation value") + info(f"Using {ud_value} as the user-defined attestation value") # Attempt to unlock the device without exiting the UI try: @@ -98,7 +98,10 @@ def do_attestation(options): # Signer attestation info("Gathering Signer attestation... ", options.verbose) try: - signer_attestation = hsm.get_signer_attestation() + powhsm_attestation = hsm.get_powhsm_attestation(ud_value) + # Health check: message and envelope must be the same + if powhsm_attestation["message"] != powhsm_attestation["envelope"]: + raise AdminError("Signer attestation message and envelope differ") except Exception as e: raise AdminError(f"Failed to gather Signer attestation: {str(e)}") info("Signer attestation gathered") @@ -117,10 +120,10 @@ def do_attestation(options): att_cert.add_element( HSMCertificateElement({ "name": "signer", - "message": signer_attestation["message"], - "signature": signer_attestation["signature"], + "message": powhsm_attestation["message"], + "signature": powhsm_attestation["signature"], "signed_by": "attestation", - "tweak": signer_attestation["app_hash"], + "tweak": powhsm_attestation["app_hash"], })) att_cert.clear_targets() att_cert.add_target("ui") diff --git a/middleware/admin/verify_attestation.py b/middleware/admin/verify_ledger_attestation.py similarity index 75% rename from middleware/admin/verify_attestation.py rename to middleware/admin/verify_ledger_attestation.py index 3d398267..f09f99c2 100644 --- a/middleware/admin/verify_attestation.py +++ b/middleware/admin/verify_ledger_attestation.py @@ -30,13 +30,22 @@ UI_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:UI:(5.[0-9])") -SIGNER_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:SIGNER:(5.[0-9])") +SIGNER_LEGACY_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:SIGNER:(5.[0-9])") UI_DERIVATION_PATH = "m/44'/0'/0'/0/0" UD_VALUE_LENGTH = 32 PUBKEY_COMPRESSED_LENGTH = 33 SIGNER_HASH_LENGTH = 32 SIGNER_ITERATION_LENGTH = 2 +# New signer message header with fields +SIGNER_MESSAGE_HEADER_REGEX = re.compile(b"^POWHSM:(5.[0-9])::") +SM_PLATFORM_LEN = 3 +SM_UD_LEN = 32 +SM_PKH_LEN = 32 +SM_BB_LEN = 32 +SM_TXN_LEN = 8 +SM_TMSTMP_LEN = 8 + # Ledger's root authority # (according to # https://github.com/LedgerHQ/blue-loader-python/blob/master/ledgerblue/ @@ -46,14 +55,6 @@ "dad609" -def match_ui_message_header(ui_message): - return UI_MESSAGE_HEADER_REGEX.match(ui_message) - - -def match_signer_message_header(signer_message): - return SIGNER_MESSAGE_HEADER_REGEX.match(signer_message) - - def do_verify_attestation(options): head("### -> Verify UI and Signer attestations", fill="#") @@ -130,7 +131,7 @@ def do_verify_attestation(options): ui_message = bytes.fromhex(ui_result[1]) ui_hash = bytes.fromhex(ui_result[2]) - mh_match = match_ui_message_header(ui_message) + mh_match = UI_MESSAGE_HEADER_REGEX.match(ui_message) if mh_match is None: raise AdminError( f"Invalid UI attestation message header: {ui_message.hex()}") @@ -174,26 +175,64 @@ def do_verify_attestation(options): signer_message = bytes.fromhex(signer_result[1]) signer_hash = bytes.fromhex(signer_result[2]) - mh_match = match_signer_message_header(signer_message) - if mh_match is None: + lmh_match = SIGNER_LEGACY_MESSAGE_HEADER_REGEX.match(signer_message) + mh_match = SIGNER_MESSAGE_HEADER_REGEX.match(signer_message) + if lmh_match is None and mh_match is None: raise AdminError( f"Invalid Signer attestation message header: {signer_message.hex()}") - mh_len = len(mh_match.group(0)) - if signer_message[mh_len:] != pubkeys_hash: - reported = signer_message[mh_len:].hex() + if lmh_match is not None: + # Legacy header + hlen = len(lmh_match.group(0)) + signer_version = lmh_match.group(1) + offset = hlen + reported_pubkeys_hash = signer_message[offset:] + offset += SM_PKH_LEN + else: + # New header + hlen = len(mh_match.group(0)) + signer_version = mh_match.group(1) + offset = hlen + reported_platform = signer_message[offset:offset+SM_PLATFORM_LEN] + offset += SM_PLATFORM_LEN + reported_ud_value = signer_message[offset:offset+SM_UD_LEN] + offset += SM_UD_LEN + reported_pubkeys_hash = signer_message[offset:offset+SM_PKH_LEN] + offset += SM_PKH_LEN + reported_best_block = signer_message[offset:offset+SM_BB_LEN] + offset += SM_BB_LEN + reported_txn_head = signer_message[offset:offset+SM_TXN_LEN] + offset += SM_TXN_LEN + reported_timestamp = signer_message[offset:offset+SM_TMSTMP_LEN] + offset += SM_TMSTMP_LEN + + if signer_message[offset:] != b'': + raise AdminError(f"Signer attestation message longer " + f"than expected: {signer_message.hex()}") + + if reported_pubkeys_hash != pubkeys_hash: raise AdminError( f"Signer attestation public keys hash mismatch: expected {pubkeys_hash.hex()}" - f" but attestation reports {reported}" + f" but attestation reports {reported_pubkeys_hash.hex()}" ) - signer_version = mh_match.group(1) + signer_info = [ + f"Hash: {pubkeys_hash.hex()}", + "", + f"Installed Signer hash: {signer_hash.hex()}", + f"Installed Signer version: {signer_version.decode()}", + ] + + if mh_match is not None: + signer_info += [ + f"Platform: {reported_platform.decode("ASCII")}", + f"UD value: {reported_ud_value.hex()}", + f"Best block: {reported_best_block.hex()}", + f"Last transaction signed: {reported_txn_head.hex()}", + f"Timestamp: {reported_timestamp.hex()}", + ] + head( - ["Signer verified with public keys:"] + pubkeys_output + [ - "", - f"Hash: {signer_message[mh_len:].hex()}", - f"Installed Signer hash: {signer_hash.hex()}", - f"Installed Signer version: {signer_version.decode()}", - ], + ["Signer verified with public keys:"] + pubkeys_output + signer_info, fill="-", ) diff --git a/middleware/ledger/hsm2dongle.py b/middleware/ledger/hsm2dongle.py index 91dbcb2a..e1154b95 100644 --- a/middleware/ledger/hsm2dongle.py +++ b/middleware/ledger/hsm2dongle.py @@ -28,7 +28,7 @@ from .signature import HSM2DongleSignature from .version import HSM2FirmwareVersion from .parameters import HSM2FirmwareParameters -from .hsm2dongle_cmds import HSM2SignerHeartbeat, HSM2UIHeartbeat +from .hsm2dongle_cmds import HSM2SignerHeartbeat, HSM2UIHeartbeat, PowHsmAttestation from .block_utils import ( rlp_mm_payload_size, remove_mm_fields_if_present, @@ -63,7 +63,6 @@ class _Command(IntEnum): SEED = 0x44 WIPE = 0x07 UI_ATT = 0x50 - SIGNER_ATT = 0x50 SIGNER_AUTH = 0x51 RETRIES = 0x45 @@ -118,13 +117,6 @@ class _UIAttestationOps(IntEnum): OP_APP_HASH = 0x04 -# Signer attestation OPs -class _SignerAttestationOps(IntEnum): - OP_GET = 0x01 - OP_GET_MESSAGE = 0x02 - OP_APP_HASH = 0x03 - - # Signer authorization OPs (and results) class _SignerAuthorizationOps(IntEnum): OP_SIGVER = 0x01 @@ -141,7 +133,6 @@ class _Ops: ADVANCE = _AdvanceOps UPD_ANCESTOR = _UpdateAncestorOps UI_ATT = _UIAttestationOps - SIGNER_ATT = _SignerAttestationOps SIGNER_AUTH = _SignerAuthorizationOps @@ -251,11 +242,6 @@ class _UIAttestationError(IntEnum): INTERNAL = 0x6A99 -class _SignerAttestationError(IntEnum): - PROT_INVALID = 0x6B00 - INTERNAL = 0x6B01 - - class _SignerAuthorizationError(IntEnum): PROT_INVALID = 0x6A01 INVALID_ITERATION = 0x6a03 @@ -271,7 +257,6 @@ class _Error: UPD_ANCESTOR = _AdvanceUpdateError UI = _UIError UI_ATT = _UIAttestationError - SIGNER_ATT = _SignerAttestationError SIGNER_AUTH = _SignerAuthorizationError # Whether a given code is in the @@ -1111,27 +1096,8 @@ def get_ui_attestation(self, ud_value_hex): "signature": attestation.hex(), } - def get_signer_attestation(self): - # Get signer hash - signer_hash = self._send_command( - self.CMD.SIGNER_ATT, bytes([self.OP.SIGNER_ATT.OP_APP_HASH]) - )[self.OFF.DATA:] - - # Retrieve attestation - attestation = self._send_command( - self.CMD.SIGNER_ATT, bytes([self.OP.SIGNER_ATT.OP_GET]) - )[self.OFF.DATA:] - - # Retrieve message - message = self._send_command( - self.CMD.SIGNER_ATT, bytes([self.OP.SIGNER_ATT.OP_GET_MESSAGE]) - )[self.OFF.DATA:] - - return { - "app_hash": signer_hash.hex(), - "message": message.hex(), - "signature": attestation.hex(), - } + def get_powhsm_attestation(self, ud_value_hex): + return PowHsmAttestation(self).run(ud_value_hex) def get_signer_heartbeat(self, ud_value): return HSM2SignerHeartbeat(self).run(ud_value) diff --git a/middleware/ledger/hsm2dongle_cmds/__init__.py b/middleware/ledger/hsm2dongle_cmds/__init__.py index 336f5b83..a6471ba5 100644 --- a/middleware/ledger/hsm2dongle_cmds/__init__.py +++ b/middleware/ledger/hsm2dongle_cmds/__init__.py @@ -22,3 +22,4 @@ from .signer_heartbeat import HSM2SignerHeartbeat from .ui_heartbeat import HSM2UIHeartbeat +from .powhsm_attestation import PowHsmAttestation diff --git a/middleware/ledger/hsm2dongle_cmds/command.py b/middleware/ledger/hsm2dongle_cmds/command.py index b1825a95..c33e2ae2 100644 --- a/middleware/ledger/hsm2dongle_cmds/command.py +++ b/middleware/ledger/hsm2dongle_cmds/command.py @@ -34,7 +34,7 @@ def __init__(self, hsm2dongle): self.Offset = hsm2dongle.OFF self.ErrorResult = hsm2dongle.ErrorResult - def send(self, op, data, timeout=None): + def send(self, op, data=b'', timeout=None): # Default timeout if timeout is None: timeout = self.dongle.DONGLE_TIMEOUT diff --git a/middleware/ledger/hsm2dongle_cmds/powhsm_attestation.py b/middleware/ledger/hsm2dongle_cmds/powhsm_attestation.py new file mode 100644 index 00000000..f5dff5b0 --- /dev/null +++ b/middleware/ledger/hsm2dongle_cmds/powhsm_attestation.py @@ -0,0 +1,81 @@ +# 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. + +from enum import IntEnum +from .command import HSM2DongleCommand + + +class Op(IntEnum): + OP_GET = 0x01 + OP_GET_MESSAGE = 0x02 + OP_APP_HASH = 0x03 + OP_GET_ENVELOPE = 0x04 + + +LEGACY_HEADER = b"HSM:SIGNER:" + + +# Implements the powhsm attestation protocol against a +# running powhsm +class PowHsmAttestation(HSM2DongleCommand): + Command = 0x50 + + def run(self, ud_value_hex): + # Retrieve attestation signature + signature = self.send(Op.OP_GET, + bytes.fromhex(ud_value_hex))[self.Offset.DATA:] + + # Retrieve message and envelope + bufs = {} + brk = False + msgoffset = 1 # For legacy behavior handling + for (op, name) in \ + [(Op.OP_GET_MESSAGE, "message"), (Op.OP_GET_ENVELOPE, "envelope")]: + # Legacy behavior handling + if brk: + bufs["envelope"] = bufs["message"] + break + bufs[name] = b'' + more = True + page = 0 + while more: + result = self.send(op, bytes([page])) + more = result[self.Offset.DATA] == 1 + # Legacy behavior handling + if name == "message" and \ + result[self.Offset.DATA:self.Offset.DATA+len(LEGACY_HEADER)] == \ + LEGACY_HEADER: + msgoffset = 0 + more = False + brk = True + bufs[name] += result[self.Offset.DATA+msgoffset:] + page += 1 + + # Get signer hash + signer_hash = self.send(Op.OP_APP_HASH)[self.Offset.DATA:] + + return { + "app_hash": signer_hash.hex(), + "envelope": bufs["envelope"].hex(), + "message": bufs["message"].hex(), + "signature": signature.hex(), + } diff --git a/middleware/tests/admin/test_attestation.py b/middleware/tests/admin/test_ledger_attestation.py similarity index 89% rename from middleware/tests/admin/test_attestation.py rename to middleware/tests/admin/test_ledger_attestation.py index f2edfd59..935b1cc2 100644 --- a/middleware/tests/admin/test_attestation.py +++ b/middleware/tests/admin/test_ledger_attestation.py @@ -25,7 +25,7 @@ from types import SimpleNamespace from unittest import TestCase from unittest.mock import Mock, call, patch, mock_open -from admin.attestation import do_attestation +from admin.ledger_attestation import do_attestation from admin.certificate import HSMCertificate from admin.misc import AdminError from admin.rsk_client import RskClientError @@ -33,9 +33,9 @@ @patch("sys.stdout.write") @patch("time.sleep") -@patch("admin.attestation.do_unlock") -@patch("admin.attestation.get_hsm") -@patch("admin.attestation.HSMCertificate.from_jsonfile") +@patch("admin.ledger_attestation.do_unlock") +@patch("admin.ledger_attestation.get_hsm") +@patch("admin.ledger_attestation.HSMCertificate.from_jsonfile") class TestAttestation(TestCase): def setupMocks(self, from_jsonfile, get_hsm): from_jsonfile.return_value = HSMCertificate({ @@ -64,7 +64,8 @@ def setupMocks(self, from_jsonfile, get_hsm): }) hsm.exit_menu = Mock() hsm.disconnect = Mock() - hsm.get_signer_attestation = Mock(return_value={ + hsm.get_powhsm_attestation = Mock(return_value={ + 'envelope': 'dd' * 32, 'message': 'dd' * 32, 'signature': 'ee' * 32, 'app_hash': 'ff' * 32 @@ -78,7 +79,7 @@ def setupDefaultOptions(self): options.attestation_ud_source = 'aa' * 32 return options - @patch('admin.attestation.RskClient') + @patch('admin.ledger_attestation.RskClient') def test_attestation_ok_provided_ud_value(self, RskClient, from_jsonfile, @@ -134,7 +135,7 @@ def test_attestation_ok_provided_ud_value(self, }, indent=2))], file_mock.return_value.write.call_args_list) - @patch('admin.attestation.RskClient') + @patch('admin.ledger_attestation.RskClient') def test_attestation_ok_get_ud_value(self, RskClient, from_jsonfile, get_hsm, *_): self.setupMocks(from_jsonfile, get_hsm) RskClient.return_value = Mock() @@ -227,7 +228,7 @@ def test_attestation_invalid_jsonfile(self, from_jsonfile, get_hsm, *_): self.assertFalse(get_hsm.called) self.assertFalse(file_mock.return_value.write.called) - @patch('admin.attestation.RskClient') + @patch('admin.ledger_attestation.RskClient') def test_attestation_rsk_client_error(self, RskClient, from_jsonfile, get_hsm, *_): self.setupMocks(from_jsonfile, get_hsm) RskClient.side_effect = RskClientError('error-msg') @@ -274,10 +275,10 @@ def test_attestation_get_ui_attestation_error(self, from_jsonfile, get_hsm, *_): self.assertTrue(get_hsm.called) self.assertFalse(file_mock.return_value.write.called) - def test_attestation_get_signer_attestation_error(self, from_jsonfile, get_hsm, *_): + def test_attestation_get_powhsm_attestation_error(self, from_jsonfile, get_hsm, *_): self.setupMocks(from_jsonfile, get_hsm) hsm = get_hsm.return_value - hsm.get_signer_attestation.side_effect = Exception() + hsm.get_powhsm_attestation.side_effect = Exception() options = self.setupDefaultOptions() with patch('builtins.open', mock_open()) as file_mock: with self.assertRaises(AdminError): @@ -286,7 +287,21 @@ def test_attestation_get_signer_attestation_error(self, from_jsonfile, get_hsm, self.assertTrue(get_hsm.called) self.assertFalse(file_mock.return_value.write.called) - @patch("admin.attestation.HSMCertificate.add_element") + def test_attestation_get_powhsm_attestation_envelope_msg_differ(self, from_jsonfile, + get_hsm, *_): + self.setupMocks(from_jsonfile, get_hsm) + hsm = get_hsm.return_value + hsm.get_powhsm_attestation.return_value["envelope"] = "11"*32 + hsm.get_powhsm_attestation.return_value["message"] = "22"*32 + options = self.setupDefaultOptions() + with patch('builtins.open', mock_open()) as file_mock: + with self.assertRaises(AdminError): + do_attestation(options) + self.assertTrue(from_jsonfile.called) + self.assertTrue(get_hsm.called) + self.assertFalse(file_mock.return_value.write.called) + + @patch("admin.ledger_attestation.HSMCertificate.add_element") def test_attestation_add_element_error(self, add_element, from_jsonfile, get_hsm, *_): self.setupMocks(from_jsonfile, get_hsm) add_element.side_effect = Exception() @@ -298,7 +313,7 @@ def test_attestation_add_element_error(self, add_element, from_jsonfile, get_hsm self.assertTrue(get_hsm.called) self.assertFalse(file_mock.return_value.write.called) - @patch("admin.attestation.HSMCertificate.add_target") + @patch("admin.ledger_attestation.HSMCertificate.add_target") def test_attestation_add_target_error(self, add_target, from_jsonfile, get_hsm, *_): self.setupMocks(from_jsonfile, get_hsm) add_target.side_effect = ValueError() @@ -310,7 +325,7 @@ def test_attestation_add_target_error(self, add_target, from_jsonfile, get_hsm, self.assertTrue(get_hsm.called) self.assertFalse(file_mock.return_value.write.called) - @patch("admin.attestation.HSMCertificate.save_to_jsonfile") + @patch("admin.ledger_attestation.HSMCertificate.save_to_jsonfile") def test_attestation_save_to_jsonfile_error(self, save_to_jsonfile, from_jsonfile, diff --git a/middleware/tests/admin/test_verify_attestation.py b/middleware/tests/admin/test_verify_ledger_attestation.py similarity index 68% rename from middleware/tests/admin/test_verify_attestation.py rename to middleware/tests/admin/test_verify_ledger_attestation.py index 665e96db..40f510e2 100644 --- a/middleware/tests/admin/test_verify_attestation.py +++ b/middleware/tests/admin/test_verify_ledger_attestation.py @@ -25,11 +25,7 @@ from unittest.mock import Mock, call, patch, mock_open from admin.misc import AdminError from admin.pubkeys import PATHS -from admin.verify_attestation import ( - do_verify_attestation, - match_ui_message_header, - match_signer_message_header -) +from admin.verify_ledger_attestation import do_verify_attestation import ecdsa import hashlib import logging @@ -37,7 +33,8 @@ logging.disable(logging.CRITICAL) EXPECTED_UI_DERIVATION_PATH = "m/44'/0'/0'/0/0" -SIGNER_HEADER = b"HSM:SIGNER:5.3" +LEGACY_SIGNER_HEADER = b"HSM:SIGNER:5.3" +POWHSM_HEADER = b"POWHSM:5.4::" UI_HEADER = b"HSM:UI:5.3" @@ -78,16 +75,71 @@ def setUp(self): bytes.fromhex("0123") self.ui_hash = bytes.fromhex("ee" * 32) - self.signer_msg = SIGNER_HEADER + \ - bytes.fromhex(self.pubkeys_hash.hex()) + self.signer_msg = POWHSM_HEADER + \ + b'plf' + \ + bytes.fromhex('aa'*32) + \ + bytes.fromhex(self.pubkeys_hash.hex()) + \ + bytes.fromhex('bb'*32) + \ + bytes.fromhex('cc'*8) + \ + bytes.fromhex('dd'*8) + self.signer_hash = bytes.fromhex("ff" * 32) self.result = {} self.result['ui'] = (True, self.ui_msg.hex(), self.ui_hash.hex()) self.result['signer'] = (True, self.signer_msg.hex(), self.signer_hash.hex()) - @patch("admin.verify_attestation.head") - @patch("admin.verify_attestation.HSMCertificate") + @patch("admin.verify_ledger_attestation.head") + @patch("admin.verify_ledger_attestation.HSMCertificate") + @patch("json.loads") + def test_verify_attestation_legacy(self, + loads_mock, + certificate_mock, + head_mock, _): + self.signer_msg = LEGACY_SIGNER_HEADER + \ + bytes.fromhex(self.pubkeys_hash.hex()) + self.signer_hash = bytes.fromhex("ff" * 32) + self.result['signer'] = (True, self.signer_msg.hex(), self.signer_hash.hex()) + + loads_mock.return_value = self.public_keys + att_cert = Mock() + att_cert.validate_and_get_values = Mock(return_value=self.result) + certificate_mock.from_jsonfile = Mock(return_value=att_cert) + + with patch('builtins.open', mock_open(read_data='')) as file_mock: + do_verify_attestation(self.default_options) + + self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + self.assertEqual([call(self.certification_path)], + certificate_mock.from_jsonfile.call_args_list) + + expected_call_ui = call( + [ + "UI verified with:", + f"UD value: {'aa'*32}", + f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): {'bb'*33}", + f"Authorized signer hash: {'cc'*32}", + "Authorized signer iteration: 291", + f"Installed UI hash: {'ee'*32}", + "Installed UI version: 5.3", + ], + fill="-", + ) + self.assertEqual(expected_call_ui, head_mock.call_args_list[1]) + + expected_call_signer = call( + ["Signer verified with public keys:"] + self.expected_pubkeys_output + [ + f"Hash: {self.pubkeys_hash.hex()}", + "", + f"Installed Signer hash: {'ff'*32}", + "Installed Signer version: 5.3", + ], + fill="-", + ) + self.assertEqual(expected_call_signer, head_mock.call_args_list[2]) + + @patch("admin.verify_ledger_attestation.head") + @patch("admin.verify_ledger_attestation.HSMCertificate") @patch("json.loads") def test_verify_attestation(self, loads_mock, @@ -122,10 +174,18 @@ def test_verify_attestation(self, expected_call_signer = call( ["Signer verified with public keys:"] + self.expected_pubkeys_output + [ - "", f"Hash: {self.pubkeys_hash.hex()}", - f"Installed Signer hash: {'ff'*32}", - "Installed Signer version: 5.3", + "", + "Installed Signer hash: ffffffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffff", + "Installed Signer version: 5.4", + "Platform: plf", + "UD value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaa", + "Best block: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbbb", + "Last transaction signed: cccccccccccccccc", + "Timestamp: dddddddddddddddd", ], fill="-", ) @@ -182,7 +242,7 @@ def test_verify_attestation_no_ui_derivation_key(self, loads_mock, _): 'not present in public key file'), str(e.exception)) - @patch("admin.verify_attestation.HSMCertificate") + @patch("admin.verify_ledger_attestation.HSMCertificate") @patch("json.loads") def test_verify_attestation_invalid_certificate(self, loads_mock, @@ -199,7 +259,7 @@ def test_verify_attestation_invalid_certificate(self, self.assertEqual('While loading the attestation certificate file: error-msg', str(e.exception)) - @patch("admin.verify_attestation.HSMCertificate") + @patch("admin.verify_ledger_attestation.HSMCertificate") @patch("json.loads") def test_verify_attestation_no_ui_att(self, loads_mock, @@ -221,7 +281,7 @@ def test_verify_attestation_no_ui_att(self, self.assertEqual('Certificate does not contain a UI attestation', str(e.exception)) - @patch("admin.verify_attestation.HSMCertificate") + @patch("admin.verify_ledger_attestation.HSMCertificate") @patch("json.loads") def test_verify_attestation_invalid_ui_att(self, loads_mock, @@ -242,7 +302,7 @@ def test_verify_attestation_invalid_ui_att(self, self.assertEqual("Invalid UI attestation: error validating 'ui'", str(e.exception)) - @patch("admin.verify_attestation.HSMCertificate") + @patch("admin.verify_ledger_attestation.HSMCertificate") @patch("json.loads") def test_verify_attestation_no_signer_att(self, loads_mock, @@ -264,7 +324,7 @@ def test_verify_attestation_no_signer_att(self, self.assertEqual('Certificate does not contain a Signer attestation', str(e.exception)) - @patch("admin.verify_attestation.HSMCertificate") + @patch("admin.verify_ledger_attestation.HSMCertificate") @patch("json.loads") def test_verify_attestation_invalid_signer_att(self, loads_mock, @@ -285,44 +345,43 @@ def test_verify_attestation_invalid_signer_att(self, self.assertEqual(("Invalid Signer attestation: error validating 'signer'"), str(e.exception)) - def test_match_ui_message_header_valid_header(self, _): - valid_headers = [ - UI_HEADER, - b"HSM:UI:5.0", - b"HSM:UI:5.5", - b"HSM:UI:5.9", - ] - for header in valid_headers: - ui_message = header + self.ui_msg[len(UI_HEADER):] - self.assertTrue(match_ui_message_header(ui_message)) - - def test_match_ui_message_header_invalid_header(self, _): - invalid_headers = [ - SIGNER_HEADER, - b"HSM:UI:4.0", - b"HSM:UI:5.X", - ] - for header in invalid_headers: - ui_message = header + self.ui_msg[len(UI_HEADER):] - self.assertFalse(match_ui_message_header(ui_message)) - - def test_match_signer_message_header_valid_header(self, _): - valid_headers = [ - SIGNER_HEADER, - b"HSM:SIGNER:5.0", - b"HSM:SIGNER:5.5", - b"HSM:SIGNER:5.9", - ] - for header in valid_headers: - signer_message = header + self.signer_msg[len(SIGNER_HEADER):] - self.assertTrue(match_signer_message_header(signer_message)) - - def test_match_signer_message_header_invalid_header(self, _): - invalid_headers = [ - UI_HEADER, - b"HSM:SIGNER:4.0", - b"HSM:SIGNER:5.X", - ] - for header in invalid_headers: - signer_message = header + self.signer_msg[len(SIGNER_HEADER):] - self.assertFalse(match_signer_message_header(signer_message)) + @patch("admin.verify_ledger_attestation.HSMCertificate") + @patch("json.loads") + def test_verify_attestation_invalid_signer_att_header(self, + loads_mock, + certificate_mock, _): + loads_mock.return_value = self.public_keys + signer_header = b"POWHSM:AAA::somerandomstuff".hex() + self.result["signer"] = (True, signer_header, self.signer_hash.hex()) + att_cert = Mock() + att_cert.validate_and_get_values = Mock(return_value=self.result) + certificate_mock.from_jsonfile = Mock(return_value=att_cert) + + with patch('builtins.open', mock_open(read_data='')) as file_mock: + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) + + self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + self.assertEqual((f"Invalid Signer attestation message header: {signer_header}"), + str(e.exception)) + + @patch("admin.verify_ledger_attestation.HSMCertificate") + @patch("json.loads") + def test_verify_attestation_invalid_signer_att_msg_too_long(self, + loads_mock, + certificate_mock, _): + loads_mock.return_value = self.public_keys + signer_header = (b"POWHSM:5.9::" + b"aa"*300).hex() + self.result["signer"] = (True, signer_header, self.signer_hash.hex()) + att_cert = Mock() + att_cert.validate_and_get_values = Mock(return_value=self.result) + certificate_mock.from_jsonfile = Mock(return_value=att_cert) + + with patch('builtins.open', mock_open(read_data='')) as file_mock: + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) + + self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + self.assertEqual(("Signer attestation message longer " + f"than expected: {signer_header}"), + str(e.exception)) diff --git a/middleware/tests/ledger/hsm2dongle_cmds/test_powhsm_attestation.py b/middleware/tests/ledger/hsm2dongle_cmds/test_powhsm_attestation.py new file mode 100644 index 00000000..2a280150 --- /dev/null +++ b/middleware/tests/ledger/hsm2dongle_cmds/test_powhsm_attestation.py @@ -0,0 +1,121 @@ +# 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. + +from ..test_hsm2dongle import TestHSM2DongleBase +from ledger.hsm2dongle import HSM2DongleErrorResult, HSM2DongleError +from ledgerblue.commException import CommException + +import logging + +logging.disable(logging.CRITICAL) + + +class TestPowHsmAttestation(TestHSM2DongleBase): + SIG = "3046022100e4c30ef37a1228a2faf2a88c8fb52a1dfe006a222d0961" \ + "c43792018481d0d5e2022100b206abd9c8a46336f9684a84083613fb" \ + "e4d31c34f7c023e5716545a00a709318" + + def test_ok(self): + self.dongle.exchange.side_effect = [ + bytes.fromhex("aabbcc" + self.SIG), + bytes.fromhex("aabbcc01112233445566778899"), + bytes.fromhex("aabbcc00aabbccddeeff"), + bytes.fromhex("aabbcc0112345678"), + bytes.fromhex("aabbcc019abcdef0"), + bytes.fromhex("aabbcc001122334455"), + bytes.fromhex("aabbcc334455667788aabbccdd"), + ] + + self.assertEqual({ + "message": "112233445566778899aabbccddeeff", + "envelope": "123456789abcdef01122334455", + "app_hash": "334455667788aabbccdd", + "signature": self.SIG, + }, self.hsm2dongle.get_powhsm_attestation("aa" + "bb"*30 + "cc")) + + self.assert_exchange([ + bytes.fromhex("5001aa" + "bb"*30 + "cc"), + bytes.fromhex("500200"), + bytes.fromhex("500201"), + bytes.fromhex("500400"), + bytes.fromhex("500401"), + bytes.fromhex("500402"), + bytes.fromhex("5003"), + ]) + + def test_legacy_ok(self): + self.dongle.exchange.side_effect = [ + bytes.fromhex("aabbcc" + self.SIG), + bytes.fromhex("aabbcc") + b"HSM:SIGNER:5.0morestuff", + bytes.fromhex("aabbcc334455667788aabbccdd"), + ] + + self.assertEqual({ + "message": b"HSM:SIGNER:5.0morestuff".hex(), + "envelope": b"HSM:SIGNER:5.0morestuff".hex(), + "app_hash": "334455667788aabbccdd", + "signature": self.SIG, + }, self.hsm2dongle.get_powhsm_attestation("aa" + "bb"*30 + "cc")) + + self.assert_exchange([ + bytes.fromhex("5001aa" + "bb"*30 + "cc"), + bytes.fromhex("500200"), + bytes.fromhex("5003"), + ]) + + def test_error_result(self): + self.dongle.exchange.side_effect = [ + bytes.fromhex("aabbcc" + self.SIG), + bytes.fromhex("aabbcc01112233445566778899"), + bytes.fromhex("aabbcc00aabbccddeeff"), + CommException("an-error-result", 0x6b01) + ] + + with self.assertRaises(HSM2DongleErrorResult) as e: + self.hsm2dongle.get_powhsm_attestation("aa" + "bb"*30 + "cc") + self.assertEqual(e.exception.error_code, 0x6b01) + + self.assert_exchange([ + bytes.fromhex("5001aa" + "bb"*30 + "cc"), + bytes.fromhex("500200"), + bytes.fromhex("500201"), + bytes.fromhex("500400"), + ]) + + def test_exception(self): + self.dongle.exchange.side_effect = [ + bytes.fromhex("aabbcc" + self.SIG), + bytes.fromhex("aabbcc01112233445566778899"), + bytes.fromhex("aabbcc00aabbccddeeff"), + CommException("an-exception") + ] + + with self.assertRaises(HSM2DongleError) as e: + self.hsm2dongle.get_powhsm_attestation("aa" + "bb"*30 + "cc") + self.assertIn("an-exception", e.exception.message) + + self.assert_exchange([ + bytes.fromhex("5001aa" + "bb"*30 + "cc"), + bytes.fromhex("500200"), + bytes.fromhex("500201"), + bytes.fromhex("500400"), + ]) From fc5aab672b3ad238fd7f8dfde78cc20f6990ae53 Mon Sep 17 00:00:00 2001 From: Italo Sampaio <100376888+italo-sampaio@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:21:25 -0300 Subject: [PATCH 05/21] Fixes coverage report for feature/sgx branch (#224) - Triggers the coverage workflow for pushes to master and feature/sgx branches - Adds optional exec argument unit tests scripts - Some additional fixes to unit tests --- .github/workflows/coverage.yml | 2 +- firmware/coverage/gen-coverage | 16 +++++------ firmware/src/common/test/run-all.sh | 30 ++++++++++++-------- firmware/src/hal/common/test/run-all.sh | 30 ++++++++++++-------- firmware/src/hal/sgx/test/run-all.sh | 29 ++++++++++++------- firmware/src/hal/x86/test/run-all.sh | 29 ++++++++++++------- firmware/src/ledger/signer/test/run-all.sh | 30 ++++++++++++-------- firmware/src/ledger/ui/test/run-all.sh | 31 +++++++++++++-------- firmware/src/powhsm/test/btctx/test_btctx.c | 6 ++-- firmware/src/powhsm/test/run-all.sh | 30 ++++++++++++-------- firmware/src/powhsm/test/srlp/test_srlp.c | 5 +++- firmware/src/sgx/test/run-all.sh | 30 ++++++++++++-------- 12 files changed, 170 insertions(+), 98 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8a688477..8169f121 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,7 +2,7 @@ name: "Code coverage" on: push: - branches: [ "master" ] + branches: [ "master", "feature/sgx" ] jobs: coverage: diff --git a/firmware/coverage/gen-coverage b/firmware/coverage/gen-coverage index e8d4e6cc..70859ef8 100755 --- a/firmware/coverage/gen-coverage +++ b/firmware/coverage/gen-coverage @@ -10,13 +10,13 @@ if [[ $1 == "exec" ]]; then find $REPOROOT/firmware -name "*.gcno" -o -name "*.gcda" | xargs rm -f # Run unit tests with coverage generation - COVERAGE=y $REPOROOT/firmware/src/common/test/run-all.sh - COVERAGE=y $REPOROOT/firmware/src/powhsm/test/run-all.sh - COVERAGE=y $REPOROOT/firmware/src/sgx/test/run-all.sh - COVERAGE=y $REPOROOT/firmware/src/ledger/ui/test/run-all.sh - COVERAGE=y $REPOROOT/firmware/src/ledger/signer/test/run-all.sh - COVERAGE=y $REPOROOT/firmware/src/tcpsigner/test/run-all.sh - COVERAGE=y $REPOROOT/firmware/src/hal/sgx/test/run-all.sh + # The `exec` argument is used for all scripts, since we are running them inside a docker container + COVERAGE=y $REPOROOT/firmware/src/common/test/run-all.sh exec + COVERAGE=y $REPOROOT/firmware/src/powhsm/test/run-all.sh exec + COVERAGE=y $REPOROOT/firmware/src/sgx/test/run-all.sh exec + COVERAGE=y $REPOROOT/firmware/src/ledger/ui/test/run-all.sh exec + COVERAGE=y $REPOROOT/firmware/src/ledger/signer/test/run-all.sh exec + COVERAGE=y $REPOROOT/firmware/src/hal/sgx/test/run-all.sh exec # Run tcpsigner test suite pushd $REPOROOT/firmware/src/tcpsigner > /dev/null @@ -37,7 +37,7 @@ if [[ $1 == "exec" ]]; then # Remove unwanted coverage info (test files, tcpsigner, x86 HAL implementation, mock files) lcov --remove $BASEDIR/coverage.info "*/test_*.c" --output-file $BASEDIR/coverage.info lcov --remove $BASEDIR/coverage.info "*/tcpsigner/src/*" --output-file $BASEDIR/coverage.info - lcov --remove $BASEDIR/coverage.info "*/hal/src/x86/*" --output-file $BASEDIR/coverage.info + lcov --remove $BASEDIR/coverage.info "*/hal/x86/src/*" --output-file $BASEDIR/coverage.info lcov --remove $BASEDIR/coverage.info "*/mock_*.c" --output-file $BASEDIR/coverage.info # Generate report and summary genhtml $BASEDIR/coverage.info --output $BASEDIR/output -p $SRCDIR -t "powHSM firmware" diff --git a/firmware/src/common/test/run-all.sh b/firmware/src/common/test/run-all.sh index 0ac28ed5..f5efe428 100755 --- a/firmware/src/common/test/run-all.sh +++ b/firmware/src/common/test/run-all.sh @@ -1,13 +1,21 @@ #!/bin/bash -BASEDIR=$(dirname $0) -TESTDIRS="bigdigits_helper ints memutil" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - cd "$BASEDIR/$d" - make clean test || exit $? - cd - > /dev/null -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="bigdigits_helper ints memutil" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi diff --git a/firmware/src/hal/common/test/run-all.sh b/firmware/src/hal/common/test/run-all.sh index 4278c458..650caf44 100755 --- a/firmware/src/hal/common/test/run-all.sh +++ b/firmware/src/hal/common/test/run-all.sh @@ -1,13 +1,21 @@ #!/bin/bash -BASEDIR=$(dirname $0) -TESTDIRS="sha256" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - cd "$BASEDIR/$d" - make clean test || exit $? - cd - > /dev/null -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="sha256" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../../) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi diff --git a/firmware/src/hal/sgx/test/run-all.sh b/firmware/src/hal/sgx/test/run-all.sh index c7ce071e..d059b586 100755 --- a/firmware/src/hal/sgx/test/run-all.sh +++ b/firmware/src/hal/sgx/test/run-all.sh @@ -1,12 +1,21 @@ #!/bin/bash -ROOTDIR=$(dirname $0)/../../../../.. -TESTDIR=$(realpath $(dirname $0) --relative-to $ROOTDIR) -TESTDIRS="nvmem secret_store seed" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - $ROOTDIR/docker/mware/do-notty-nousb /hsm2/$TESTDIR/$d "make clean test" || exit $? -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="nvmem secret_store seed" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../..) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi diff --git a/firmware/src/hal/x86/test/run-all.sh b/firmware/src/hal/x86/test/run-all.sh index 54e1f7d0..0628d598 100755 --- a/firmware/src/hal/x86/test/run-all.sh +++ b/firmware/src/hal/x86/test/run-all.sh @@ -1,12 +1,21 @@ #!/bin/bash -ROOTDIR=$(dirname $0)/../../../../.. -TESTDIR=$(realpath $(dirname $0) --relative-to $ROOTDIR) -TESTDIRS="bip32 endian hmac_sha256 hmac_sha512 keccak256" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - $ROOTDIR/docker/mware/do-notty-nousb /hsm2/$TESTDIR/$d "make clean test" || exit $? -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="bip32 endian hmac_sha256 hmac_sha512 keccak256" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../..) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi diff --git a/firmware/src/ledger/signer/test/run-all.sh b/firmware/src/ledger/signer/test/run-all.sh index de927848..7631c557 100755 --- a/firmware/src/ledger/signer/test/run-all.sh +++ b/firmware/src/ledger/signer/test/run-all.sh @@ -1,13 +1,21 @@ #!/bin/bash -BASEDIR=$(dirname $0) -TESTDIRS="signer_ux" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - cd "$BASEDIR/$d" - make clean test || exit $? - cd - > /dev/null -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="signer_ux" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../../) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi diff --git a/firmware/src/ledger/ui/test/run-all.sh b/firmware/src/ledger/ui/test/run-all.sh index 2be0d676..f7fd121f 100755 --- a/firmware/src/ledger/ui/test/run-all.sh +++ b/firmware/src/ledger/ui/test/run-all.sh @@ -1,13 +1,22 @@ #!/bin/bash -BASEDIR=$(dirname $0) -TESTDIRS="attestation bootloader onboard pin signer_authorization ui_comm ui_heartbeat unlock ux_handlers" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - cd "$BASEDIR/$d" - make clean test || exit $? - cd - > /dev/null -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="attestation bootloader onboard pin signer_authorization ui_comm ui_heartbeat unlock ux_handlers" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../../) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi + diff --git a/firmware/src/powhsm/test/btctx/test_btctx.c b/firmware/src/powhsm/test/btctx/test_btctx.c index 0bea267d..8b372376 100644 --- a/firmware/src/powhsm/test/btctx/test_btctx.c +++ b/firmware/src/powhsm/test/btctx/test_btctx.c @@ -54,12 +54,14 @@ int read_hex_file(const char* file_name, unsigned char** buffer, size_t* len) { fread(tmp, 2, 1, f); read_hex(tmp, 2, *buffer + off); } - fclose(f); - if (ferror(f)) { return -1; } + if (fclose(f)) { + return -1; + } + return 0; } diff --git a/firmware/src/powhsm/test/run-all.sh b/firmware/src/powhsm/test/run-all.sh index 3eb6b08e..dbe1dc1d 100755 --- a/firmware/src/powhsm/test/run-all.sh +++ b/firmware/src/powhsm/test/run-all.sh @@ -1,13 +1,21 @@ #!/bin/bash -BASEDIR=$(dirname $0) -TESTDIRS="btcscript btctx difficulty srlp svarint trie" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - cd "$BASEDIR/$d" - make clean test || exit $? - cd - > /dev/null -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="btcscript btctx difficulty srlp svarint trie" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi diff --git a/firmware/src/powhsm/test/srlp/test_srlp.c b/firmware/src/powhsm/test/srlp/test_srlp.c index b05a2b96..dba0d1ee 100644 --- a/firmware/src/powhsm/test/srlp/test_srlp.c +++ b/firmware/src/powhsm/test/srlp/test_srlp.c @@ -204,12 +204,15 @@ int read_block_file(const char* file_name, char** buffer, size_t* len) { *buffer = malloc(*len); fread(*buffer, *len, 1, f); - fclose(f); if (ferror(f)) { return -1; } + if (fclose(f)) { + return -1; + } + return 0; } diff --git a/firmware/src/sgx/test/run-all.sh b/firmware/src/sgx/test/run-all.sh index ea5c2acc..e69b07d7 100755 --- a/firmware/src/sgx/test/run-all.sh +++ b/firmware/src/sgx/test/run-all.sh @@ -1,13 +1,21 @@ #!/bin/bash -BASEDIR=$(dirname $0) -TESTDIRS="system" -TESTDIRS=${1:-"$TESTDIRS"} -for d in $TESTDIRS; do - echo "******************************" - echo "Testing $d..." - echo "******************************" - cd "$BASEDIR/$d" - make clean test || exit $? - cd - > /dev/null -done +if [[ $1 == "exec" ]]; then + BASEDIR=$(realpath $(dirname $0)) + TESTDIRS="system" + for d in $TESTDIRS; do + echo "******************************" + echo "Testing $d..." + echo "******************************" + cd "$BASEDIR/$d" + make clean test || exit $? + cd - > /dev/null + done + exit 0 +else + # Script directory + REPOROOT=$(realpath $(dirname $0)/../../../../) + SCRIPT=$(realpath $0 --relative-to=$REPOROOT) + + $REPOROOT/docker/mware/do-notty-nousb /hsm2 "./$SCRIPT exec" +fi From 54331a7e8a40a2bf624b747aac48d3bf0176e2cb Mon Sep 17 00:00:00 2001 From: Italo Sampaio <100376888+italo-sampaio@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:47:06 -0300 Subject: [PATCH 06/21] Install SGX powHSM as a systemd service (#226) --- dist/sgx/hsm/{run => start} | 7 +++- dist/sgx/hsm/stop | 4 ++ dist/sgx/scripts/hsmsgx.service | 19 +++++++++ dist/sgx/scripts/install_service | 67 ++++++++++++++++++++++++++++++++ dist/sgx/scripts/setup | 16 ++++++++ dist/sgx/setup-new-powhsm | 20 +++++++++- 6 files changed, 131 insertions(+), 2 deletions(-) rename dist/sgx/hsm/{run => start} (81%) create mode 100755 dist/sgx/hsm/stop create mode 100644 dist/sgx/scripts/hsmsgx.service create mode 100755 dist/sgx/scripts/install_service diff --git a/dist/sgx/hsm/run b/dist/sgx/hsm/start similarity index 81% rename from dist/sgx/hsm/run rename to dist/sgx/hsm/start index 44d972af..163454d0 100755 --- a/dist/sgx/hsm/run +++ b/dist/sgx/hsm/start @@ -4,6 +4,8 @@ BINDIR=$(realpath $(dirname $0)) WORKDIR=$(realpath $BINDIR/..) DOCKER_IMAGE=powhsmsgx:runner +source $BINDIR/.env + QUIET="" echo -e "\e[96mBuilding docker image $DOCKER_IMAGE (this will take a few minutes)..." if [[ "$2" != "-v" ]]; then @@ -16,10 +18,13 @@ echo DOCKER_CNT=powhsmsgx-runner DOCKER_USER="$(id -u):$(id -g)" +HOSTNAME="SGX" +NETWORK=${NETWORK:-net_sgx} PORT=7777 DOCKER_PORT="$PORT:$PORT" -docker run -ti --rm --name $DOCKER_CNT --user $DOCKER_USER -v $WORKDIR:/hsm \ +docker run --rm --name $DOCKER_CNT --user $DOCKER_USER -v $WORKDIR:/hsm \ + --hostname $HOSTNAME --network $NETWORK \ --device=/dev/sgx_enclave:/dev/sgx_enclave \ --device=/dev/sgx_provision:/dev/sgx_provision \ -w /hsm -p$DOCKER_PORT $DOCKER_IMAGE \ diff --git a/dist/sgx/hsm/stop b/dist/sgx/hsm/stop new file mode 100755 index 00000000..65d5355b --- /dev/null +++ b/dist/sgx/hsm/stop @@ -0,0 +1,4 @@ +#!/bin/bash + +DOCKER_CNT=powhsmsgx-runner +docker stop $DOCKER_CNT diff --git a/dist/sgx/scripts/hsmsgx.service b/dist/sgx/scripts/hsmsgx.service new file mode 100644 index 00000000..18c7f7e9 --- /dev/null +++ b/dist/sgx/scripts/hsmsgx.service @@ -0,0 +1,19 @@ +[Unit] +Description=SGX powHSM +Wants=network.target +After=syslog.target network-online.target docker.service +Requires=docker.service + +[Service] +Type=simple +WorkingDirectory=$HSM_INSTALL_DIR +User=hsm +Group=hsm +ExecStart=$HSM_INSTALL_DIR/bin/start +ExecStop=$HSM_INSTALL_DIR/bin/stop +Restart=on-failure +RestartSec=10 +KillMode=mixed + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/dist/sgx/scripts/install_service b/dist/sgx/scripts/install_service new file mode 100755 index 00000000..47bfb7db --- /dev/null +++ b/dist/sgx/scripts/install_service @@ -0,0 +1,67 @@ +#!/bin/bash + +# Require superuser +if ! [ "$(id -u)" == "0" ]; then + echo -e "\e[1;31mPlease run with sudo.\e[0m" + exit 1 +fi + +if [ -z "$1" ]; then + echo -e "\e[1;31mUsage: $0 \e[0m" + exit 1 +fi + +SERVICE_UNIT=$(realpath $1) +if [ ! -f "$SERVICE_UNIT" ]; then + echo "\e[1;31mService file not found: $SERVICE_UNIT\e[0m" + exit 1 +fi + +# Extract the installation directory from the service file +INSTALL_DIR=$(grep -oP 'WorkingDirectory=\K.*' $SERVICE_UNIT) +if [ -z "$INSTALL_DIR" ]; then + echo -e "\e[1;31mCould not extract installation directory from service file.\e[0m" + exit 1 +fi + +echo -e "\e[1;32mCreating hsm user and group...\e[0m" +if ! id -u hsm >/dev/null 2>&1; then + useradd -rm -s /bin/bash hsm || exit $? + usermod -aG docker hsm || exit $? +else + echo -e "\e[1;33mUser 'hsm' already exists. Skipping user creation.\e[0m" +fi + +DEFAULT_NETWORK="net_sgx" +while true; do + echo -e "\e[1;32mEnter the name of the docker network to be created: [$DEFAULT_NETWORK]\e[0m" + read -p "> " NETWORK + if [ -z "$NETWORK" ]; then + NETWORK=$DEFAULT_NETWORK + fi + echo -e "\e[1;33mThe docker network will be named '$NETWORK'. Proceed? [Y/n]\e[0m" + read -p "> " proceed + if [[ "Y" == "$proceed" ]] || [[ "y" == "$proceed" ]] || [ -z "$proceed" ]; then + break + fi +done + +echo -e "\e[1;32mCreating $NETWORK network...\e[0m" +docker network rm $NETWORK 2> /dev/null +docker network create $NETWORK &> /dev/null +echo "NETWORK=$NETWORK" >> $INSTALL_DIR/.env || exit $? + +echo -e "\e[1;32mSetting permisions...\e[0m" +chown -R root:hsm $INSTALL_DIR || exit $? +chmod 664 $INSTALL_DIR/*.dat $INSTALL_DIR/.env || exit $? + +echo -e "\e[1;32mCreating service...\e[0m" +cp $SERVICE_UNIT /etc/systemd/system/hsmsgx.service +systemctl daemon-reload || exit $? +echo -e "\e[1;32mEnabling service...\e[0m" +systemctl enable hsmsgx.service || exit $? +echo -e "\e[1;32mEStarting service...\e[0m" +systemctl start hsmsgx.service || exit $? +echo -e "\e[1;32mService started.\e[0m" +echo -e "\e[1;32mTo check the status of the service, run 'systemctl status hsmsgx.service'.\e[0m" +exit 0 diff --git a/dist/sgx/scripts/setup b/dist/sgx/scripts/setup index b118e26f..2170614d 100755 --- a/dist/sgx/scripts/setup +++ b/dist/sgx/scripts/setup @@ -35,6 +35,12 @@ EXPORT_DIR="$ROOT_DIR/export" PUBLIC_KEY_FILE="$EXPORT_DIR/public-keys.txt" PUBLIC_KEY_FILE_JSON="$EXPORT_DIR/public-keys.json" +# HSM scripts directory +SCRIPTS_DIR=$ROOT_DIR/scripts + +# Directory where the finalized systemd service unit will be saved +SERVICE_DIR=$ROOT_DIR/service + function checkHsmBinaries() { # Check for HSM binary files FILES="$HSMBIN_DIR/hsmsgx $HSMBIN_DIR/hsmsgx_enclave.signed" @@ -96,6 +102,15 @@ function selectInstallationDir() { done } +function createServiceUnit() { + rm -rf $SERVICE_DIR + mkdir $SERVICE_DIR + + cp $SCRIPTS_DIR/hsmsgx.service $SERVICE_DIR + # Replace the $HSM_INSTALL_DIR token in the script with the actual installation directory + sed -i "s|\$HSM_INSTALL_DIR|$INSTALL_DIR|g" $SERVICE_DIR/hsmsgx.service +} + function installPowHsm() { mkdir $REAL_INSTALL_DIR/bin cp -R $HSMBIN_DIR/* $REAL_INSTALL_DIR/bin @@ -134,6 +149,7 @@ checkForPinFile checkHsmBinaries expandBinaries selectInstallationDir +createServiceUnit echo echo -e "\e[1;32mInstalling the powHSM...\e[0m" installPowHsm diff --git a/dist/sgx/setup-new-powhsm b/dist/sgx/setup-new-powhsm index 1398782a..67dd81ba 100755 --- a/dist/sgx/setup-new-powhsm +++ b/dist/sgx/setup-new-powhsm @@ -1,3 +1,21 @@ #!/bin/bash -$(dirname $0)/scripts/run_with_docker ./scripts/setup $1 +# Require superuser, since we need to install a service in the host +if ! [ "$(id -u)" == "0" ]; then + echo -e "\e[1;32mPlease run with sudo.\e[0m" + exit 1 +fi + +ROOT_DIR=$(realpath $(dirname $0)) +$ROOT_DIR/scripts/run_with_docker ./scripts/setup $1 +if [ $? -ne 0 ]; then + echo -e "\e[1;31m Error during the powhsm setup, aborting \e[0m" + exit 1 +fi + +$ROOT_DIR/scripts/install_service $ROOT_DIR/service/hsmsgx.service +if [ $? -ne 0 ]; then + echo -e "\e[1;31m Error during the powhsm service installation, aborting \e[0m" + exit 1 +fi +echo -e "\e[1;32mHSM SGX setup done.\e[0m" From 344228a540ab3b1396849d8608872a880871be8c Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Tue, 3 Dec 2024 02:08:27 +1300 Subject: [PATCH 07/21] SGX endorsement and platform library (#225) - Added endorsement initialisation and finalisation functions for SGX - Added system finalisation ecall, used within the untrusted code's finalisation routine - Removed old stubs for SGX's HAL platform and endorsement - Added der_utils library to aid with endorsement signature encoding - Implemented SGX endorsement library - Implemented SGX platform library - Adjusting sync acquire lock's function to account for different return types - Removed now unused hmac_sha256 library from SGX's HAL - Added SGX's HAL der_utils and endorsement unit tests --- firmware/src/hal/include/hal/endorsement.h | 16 +- firmware/src/hal/sgx/src/trusted/der_utils.c | 62 +++ firmware/src/hal/sgx/src/trusted/der_utils.h | 28 + .../src/hal/sgx/src/trusted/endorsement.c | 352 +++++++++---- .../src/hal/sgx/src/trusted/hmac_sha256.c | 1 - .../src/hal/sgx/src/trusted/hmac_sha256.h | 1 - firmware/src/hal/sgx/src/trusted/platform.c | 49 +- firmware/src/hal/sgx/test/der_utils/Makefile | 39 ++ .../hal/sgx/test/der_utils/test_der_utils.c | 148 ++++++ .../src/hal/sgx/test/endorsement/Makefile | 39 ++ firmware/src/hal/sgx/test/endorsement/mocks.c | 137 +++++ firmware/src/hal/sgx/test/endorsement/mocks.h | 482 ++++++++++++++++++ .../sgx/test/endorsement/test_endorsement.c | 299 +++++++++++ .../mock/openenclave/attestation/attester.h | 1 + .../openenclave/attestation/sgx/evidence.h | 1 + .../mock/openenclave/attestation/verifier.h | 1 + .../test/mock/openenclave/bits/attestation.h | 1 + .../hal/sgx/test/mock/openenclave/bits/defs.h | 1 + .../test/mock/openenclave/bits/sgx/sgxtypes.h | 1 + .../hal/sgx/test/mock/openenclave/common.h | 211 +++++++- firmware/src/hal/sgx/test/run-all.sh | 2 +- firmware/src/sgx/src/hsm.edl | 2 + firmware/src/sgx/src/trusted/ecall.c | 10 +- firmware/src/sgx/src/trusted/ecall.h | 5 + firmware/src/sgx/src/trusted/sync.h | 4 +- firmware/src/sgx/src/trusted/system.c | 5 + firmware/src/sgx/src/trusted/system.h | 5 + .../src/sgx/src/untrusted/enclave_proxy.c | 15 + .../src/sgx/src/untrusted/enclave_proxy.h | 5 + firmware/src/sgx/src/untrusted/main.c | 2 +- firmware/src/sgx/test/system/test_system.c | 4 + 31 files changed, 1815 insertions(+), 114 deletions(-) create mode 100644 firmware/src/hal/sgx/src/trusted/der_utils.c create mode 100644 firmware/src/hal/sgx/src/trusted/der_utils.h delete mode 120000 firmware/src/hal/sgx/src/trusted/hmac_sha256.c delete mode 120000 firmware/src/hal/sgx/src/trusted/hmac_sha256.h mode change 120000 => 100644 firmware/src/hal/sgx/src/trusted/platform.c create mode 100644 firmware/src/hal/sgx/test/der_utils/Makefile create mode 100644 firmware/src/hal/sgx/test/der_utils/test_der_utils.c create mode 100644 firmware/src/hal/sgx/test/endorsement/Makefile create mode 100644 firmware/src/hal/sgx/test/endorsement/mocks.c create mode 100644 firmware/src/hal/sgx/test/endorsement/mocks.h create mode 100644 firmware/src/hal/sgx/test/endorsement/test_endorsement.c create mode 120000 firmware/src/hal/sgx/test/mock/openenclave/attestation/attester.h create mode 120000 firmware/src/hal/sgx/test/mock/openenclave/attestation/sgx/evidence.h create mode 120000 firmware/src/hal/sgx/test/mock/openenclave/attestation/verifier.h create mode 120000 firmware/src/hal/sgx/test/mock/openenclave/bits/attestation.h create mode 120000 firmware/src/hal/sgx/test/mock/openenclave/bits/defs.h create mode 120000 firmware/src/hal/sgx/test/mock/openenclave/bits/sgx/sgxtypes.h diff --git a/firmware/src/hal/include/hal/endorsement.h b/firmware/src/hal/include/hal/endorsement.h index 1e508e55..fbaa8adf 100644 --- a/firmware/src/hal/include/hal/endorsement.h +++ b/firmware/src/hal/include/hal/endorsement.h @@ -115,7 +115,7 @@ extern attestation_id_t attestation_id; */ bool endorsement_init(char* att_file_path); -#elif defined(HSM_PLATFORM_SGX) || defined(HSM_PLATFORM_LEDGER) +#elif defined(HSM_PLATFORM_LEDGER) /** * @brief Initializes the endorsement module @@ -124,6 +124,20 @@ bool endorsement_init(char* att_file_path); */ bool endorsement_init(); +#elif defined(HSM_PLATFORM_SGX) + +/** + * @brief Initializes the endorsement module + * + * @returns whether the initialisation succeeded + */ +bool endorsement_init(); + +/** + * @brief Finalises the endorsement module + */ +void endorsement_finalise(); + #endif // END of platform-dependent code diff --git a/firmware/src/hal/sgx/src/trusted/der_utils.c b/firmware/src/hal/sgx/src/trusted/der_utils.c new file mode 100644 index 00000000..f6841dfc --- /dev/null +++ b/firmware/src/hal/sgx/src/trusted/der_utils.c @@ -0,0 +1,62 @@ +/** + * 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. + */ + +#include "der_utils.h" +#include +#include +#include + +// Helper function to encode a len-byte unsigned integer (R or S) in DER format +static size_t der_encode_uint(uint8_t* dest, uint8_t* src, size_t len) { + // Check if we need a leading zero byte + bool lz = src[0] & 0x80; + // Start of source: remove leading zeroes + size_t trim = 0; + while (!src[trim] && trim < (len - 1)) + trim++; + // Output + size_t off = 0; + dest[off++] = 0x02; // Integer tag + dest[off++] = len - trim + (lz ? 1 : 0); // Length byte + if (lz) + dest[off++] = 0x00; // Leading zero + memcpy(dest + off, src + trim, len - trim); // Actual integer + return (size_t)dest[1] + 2; +} + +uint8_t der_encode_signature(uint8_t* dest, sgx_ecdsa256_signature_t* sig) { + uint8_t r_encoded[sizeof(sig->r) + 2], + s_encoded[sizeof(sig->s) + 2]; // Temporary buffers for R and S + uint8_t r_len = (uint8_t)der_encode_uint(r_encoded, sig->r, sizeof(sig->r)); + uint8_t s_len = (uint8_t)der_encode_uint(s_encoded, sig->s, sizeof(sig->s)); + + // Start the sequence + dest[0] = 0x30; // Sequence tag + dest[1] = r_len + s_len; // Length of the sequence + memcpy(dest + 2, r_encoded, r_len); // Copy encoded R + memcpy(dest + 2 + r_len, s_encoded, s_len); // Copy encoded S + + // Return total length of DER encoded signature + return (uint8_t)(2 + r_len + s_len); +} diff --git a/firmware/src/hal/sgx/src/trusted/der_utils.h b/firmware/src/hal/sgx/src/trusted/der_utils.h new file mode 100644 index 00000000..9f30aaff --- /dev/null +++ b/firmware/src/hal/sgx/src/trusted/der_utils.h @@ -0,0 +1,28 @@ +/** + * 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. + */ + +#include +#include + +uint8_t der_encode_signature(uint8_t* dest, sgx_ecdsa256_signature_t* sig); \ No newline at end of file diff --git a/firmware/src/hal/sgx/src/trusted/endorsement.c b/firmware/src/hal/sgx/src/trusted/endorsement.c index 2fd21e99..c5f4dc45 100644 --- a/firmware/src/hal/sgx/src/trusted/endorsement.c +++ b/firmware/src/hal/sgx/src/trusted/endorsement.c @@ -22,169 +22,315 @@ * IN THE SOFTWARE. */ -#include -#include -#include -// TODO: remove usage of secp256k1 here upon final implementation -// (only needed here for mock implementation) -#include - #include "hal/constants.h" #include "hal/endorsement.h" -#include "hal/seed.h" #include "hal/exceptions.h" #include "hal/log.h" +#include "der_utils.h" -#include "random.h" - -// TODO: remove HMAC-SHA256 entirely upon final implementation, -// (only needed for mock implementation) -#include "hmac_sha256.h" - -static secp256k1_context* sp_ctx = NULL; - -// Test key for mock implementation -static const uint8_t attestation_key[] = { - 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, - 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, - 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, -}; -// Test code hash for mock implementation -static const uint8_t attestation_code_hash[] = { - 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, - 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, - 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, -}; -static uint8_t attestation_pubkey[PUBKEY_UNCMP_LENGTH]; - -static size_t tweak_sign(const unsigned char* key, - const unsigned char* tweak, - const unsigned char* hash, - unsigned char* sig) { - unsigned char tweaked_key[PRIVATE_KEY_LENGTH]; - secp256k1_ecdsa_signature sp_sig; - size_t sig_serialized_size = MAX_SIGNATURE_LENGTH; - - // Tweak private key - memmove(tweaked_key, key, sizeof(tweaked_key)); - if (!secp256k1_ec_seckey_tweak_add(sp_ctx, tweaked_key, tweak)) - return 0; +#include - // Sign and serialize as DER - secp256k1_ecdsa_sign(sp_ctx, &sp_sig, hash, tweaked_key, NULL, NULL); - secp256k1_ecdsa_signature_serialize_der( - sp_ctx, sig, &sig_serialized_size, &sp_sig); +#include +#include +#include +#include +#include +#include + +#define RAW_ENVELOPE_BUFFER_SIZE (10 * 1024) + +static struct { + bool initialised; + // The format ID used for attestation. + // See openenclave/attestation/sgx/evidence.h for supported formats. + oe_uuid_t format_id; + // The format settings buffer for the selected format. + // This is returned by oe_verifier_get_format_settings. + uint8_t* format_settings; + // The size of the format settings buffer. + size_t format_settings_size; + // Current envelope + struct { + uint8_t raw[RAW_ENVELOPE_BUFFER_SIZE]; + size_t raw_size; + sgx_quote_t* quote; + sgx_quote_auth_data_t* quote_auth_data; + sgx_qe_auth_data_t qe_auth_data; + sgx_qe_cert_data_t qe_cert_data; + } envelope; +} G_endorsement_ctx; + +#define ENDORSEMENT_FORMAT OE_FORMAT_UUID_SGX_ECDSA + +#define ENDORSEMENT_CHECK(oe_result, error_msg) \ + { \ + if (OE_OK != oe_result) { \ + LOG(error_msg); \ + LOG(": result=%u (%s)\n", result, oe_result_str(oe_result)); \ + return false; \ + } \ + } - return (int)sig_serialized_size; +// Taken from OpenEnclave's common/sgx/quote.c +OE_INLINE uint16_t ReadUint16(const uint8_t* p) { + return (uint16_t)(p[0] | (p[1] << 8)); } -static uint8_t derive_pubkey_uncmp(const unsigned char* key, - unsigned char* dest) { - secp256k1_pubkey pubkey; - size_t dest_size = PUBKEY_UNCMP_LENGTH; +// Taken from OpenEnclave's common/sgx/quote.c +OE_INLINE uint32_t ReadUint32(const uint8_t* p) { + return (uint32_t)(p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24)); +} - // Calculate the public key and serialize it according to - // the compressed argument - if (!secp256k1_ec_pubkey_create(sp_ctx, &pubkey, key)) { - return 0; +// Generates an envelop with the provided message as a custom claim. +static bool generate_envelope(uint8_t* msg, size_t msg_size) { + if (!G_endorsement_ctx.initialised) { + LOG("Endorsement module has not been initialised\n"); + return false; } - secp256k1_ec_pubkey_serialize( - sp_ctx, dest, &dest_size, &pubkey, SECP256K1_EC_UNCOMPRESSED); + oe_result_t result = OE_FAILURE; + uint8_t* evidence_buffer = NULL; + size_t evidence_buffer_size = 0; + result = oe_get_evidence(&G_endorsement_ctx.format_id, + 0, + msg, + msg_size, + G_endorsement_ctx.format_settings, + G_endorsement_ctx.format_settings_size, + &evidence_buffer, + &evidence_buffer_size, + NULL, + NULL); + ENDORSEMENT_CHECK(result, "Envelope generation failed"); + if (evidence_buffer_size > sizeof(G_endorsement_ctx.envelope.raw)) { + LOG("Envelope generation failed: buffer needs %lu bytes but " + "only %ld available\n", + evidence_buffer_size, + sizeof(G_endorsement_ctx.envelope.raw)); + oe_free_evidence(evidence_buffer); + return false; + } - return (uint8_t)dest_size; + memcpy( + G_endorsement_ctx.envelope.raw, evidence_buffer, evidence_buffer_size); + G_endorsement_ctx.envelope.raw_size = evidence_buffer_size; + oe_free_evidence(evidence_buffer); + + return true; } -bool endorsement_init() { - // Init the secp256k1 context - if (!sp_ctx) - sp_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); - - // Compute attestation public key - if (derive_pubkey_uncmp(attestation_key, attestation_pubkey) != - PUBKEY_UNCMP_LENGTH) { - LOG("Error getting uncompressed public key for mock attestation key\n"); +// Based on OpenEnclave's common/sgx/quote.c::_parse_quote() +// No validation is performed. Left to the end user. +// Maybe we could do some minimal validation in the future. +static bool parse_envelope(uint8_t* msg, size_t msg_size) { + const uint8_t* p = G_endorsement_ctx.envelope.raw; + const uint8_t* const quote_end = p + G_endorsement_ctx.envelope.raw_size; + sgx_quote_t* _sgx_quote = (sgx_quote_t*)p; + G_endorsement_ctx.envelope.quote = _sgx_quote; + + if (quote_end < p) { + LOG("SGX quote parsing error. Pointer wrapper around\n"); + return false; + } + + p += sizeof(sgx_quote_t); + + if (p > quote_end) { + LOG("Parse error after parsing SGX quote, before signature\n"); + return false; + } + if (p + _sgx_quote->signature_len + msg_size != quote_end) { + LOG("Parse error after parsing SGX signature\n"); + return false; + } + + G_endorsement_ctx.envelope.quote_auth_data = (sgx_quote_auth_data_t*)p; + + p += sizeof(sgx_quote_auth_data_t); + + sgx_qe_auth_data_t* qe_auth_data = &G_endorsement_ctx.envelope.qe_auth_data; + qe_auth_data->size = ReadUint16(p); + p += 2; + qe_auth_data->data = (uint8_t*)p; + p += qe_auth_data->size; + + if (p > quote_end) { + LOG("Parse error after parsing QE authorization data\n"); + return false; + } + + sgx_qe_cert_data_t* qe_cert_data = &G_endorsement_ctx.envelope.qe_cert_data; + qe_cert_data->type = ReadUint16(p); + p += 2; + qe_cert_data->size = ReadUint32(p); + p += 4; + qe_cert_data->data = (uint8_t*)p; + p += qe_cert_data->size; + + if (memcmp(p, msg, msg_size)) { + LOG("Parse error: got inconsistent custom message\n"); + return false; + } + + p += msg_size; + + if (p != quote_end) { + LOG("Unexpected quote length while parsing\n"); return false; } - LOG("Loaded mock attestation key:\n"); - LOG_HEX("\tKey: ", attestation_key, sizeof(attestation_key)); - LOG_HEX("\tPublic key: ", attestation_pubkey, sizeof(attestation_pubkey)); return true; } -// TODO: Implement -uint8_t* endorsement_get_envelope() { - return NULL; +// ****************************************************** // +// ********** Public interface implemenetation ********** // +// ****************************************************** // + +bool endorsement_init() { + oe_result_t result = OE_FAILURE; + + explicit_bzero(&G_endorsement_ctx, sizeof(G_endorsement_ctx)); + + // Initialize modules + result = oe_attester_initialize(); + ENDORSEMENT_CHECK(result, "Failed to initialize attester"); + result = oe_verifier_initialize(); + ENDORSEMENT_CHECK(result, "Failed to initialize verifier"); + + // Make sure the desired format is supported and + // get the corresponding settings + const oe_uuid_t format_id = {ENDORSEMENT_FORMAT}; + result = + oe_attester_select_format(&format_id, 1, &G_endorsement_ctx.format_id); + ENDORSEMENT_CHECK(result, "Failed to select attestation format"); + + result = oe_verifier_get_format_settings( + &G_endorsement_ctx.format_id, + &G_endorsement_ctx.format_settings, + &G_endorsement_ctx.format_settings_size); + ENDORSEMENT_CHECK(result, "Format is not supported by verifier"); + + G_endorsement_ctx.initialised = true; + LOG("Attestation module initialized\n"); + return true; } -// TODO: Implement -size_t endorsement_get_envelope_length() { - return 0; +void endorsement_finalise() { + oe_verifier_shutdown(); + oe_attester_shutdown(); } bool endorsement_sign(uint8_t* msg, size_t msg_size, uint8_t* signature_out, uint8_t* signature_out_length) { - - uint8_t tweak[HMAC_SHA256_SIZE]; - uint8_t hash[HASH_LENGTH]; - if (*signature_out_length < MAX_SIGNATURE_LENGTH) { - return false; + LOG("Output buffer for signature too small: %u bytes\n", + *signature_out_length); + goto endorsement_sign_fail; } - sha256(msg, msg_size, hash, sizeof(hash)); - - if (hmac_sha256(attestation_code_hash, - sizeof(attestation_code_hash), - attestation_pubkey, - sizeof(attestation_pubkey), - tweak, - sizeof(tweak)) != sizeof(tweak)) { - LOG("Error computing tweak for endorsement\n"); - return false; + if (!generate_envelope(msg, msg_size)) { + LOG("Error generating envelope\n"); + goto endorsement_sign_fail; } - if (*signature_out_length < MAX_SIGNATURE_LENGTH) { - LOG("Output buffer for signature too small: %u bytes\n", - *signature_out_length); - return false; + if (!parse_envelope(msg, msg_size)) { + LOG("Error parsing envelope\n"); + goto endorsement_sign_fail; } - *signature_out_length = - tweak_sign(attestation_key, tweak, hash, signature_out); + // Output signature in DER format + sgx_ecdsa256_signature_t* sig = + &G_endorsement_ctx.envelope.quote_auth_data->signature; + *signature_out_length = der_encode_signature(signature_out, sig); return true; + +endorsement_sign_fail: + explicit_bzero(&G_endorsement_ctx.envelope, + sizeof(G_endorsement_ctx.envelope)); + return false; +} + +uint8_t* endorsement_get_envelope() { + if (G_endorsement_ctx.envelope.raw_size == 0) { + return 0; + } + return G_endorsement_ctx.envelope.raw; +} + +size_t endorsement_get_envelope_length() { + return G_endorsement_ctx.envelope.raw_size; } bool endorsement_get_code_hash(uint8_t* code_hash_out, uint8_t* code_hash_out_length) { + if (G_endorsement_ctx.envelope.raw_size == 0) { + LOG("No envelope available\n"); + return false; + } + + if (code_hash_out == NULL) { + LOG("Output buffer is NULL\n"); + return false; + } + if (*code_hash_out_length < HASH_LENGTH) { LOG("Output buffer for code hash too small: %u bytes\n", *code_hash_out_length); return false; } - memmove( - code_hash_out, attestation_code_hash, sizeof(attestation_code_hash)); - *code_hash_out_length = sizeof(attestation_code_hash); + memcpy(code_hash_out, + G_endorsement_ctx.envelope.quote->report_body.mrenclave, + sizeof(G_endorsement_ctx.envelope.quote->report_body.mrenclave)); + *code_hash_out_length = + sizeof(G_endorsement_ctx.envelope.quote->report_body.mrenclave); return true; } bool endorsement_get_public_key(uint8_t* public_key_out, uint8_t* public_key_out_length) { + if (G_endorsement_ctx.envelope.raw_size == 0) { + LOG("No envelope available\n"); + return false; + } + + if (public_key_out == NULL) { + LOG("Output buffer is NULL\n"); + return false; + } + if (*public_key_out_length < PUBKEY_UNCMP_LENGTH) { LOG("Output buffer for public key too small: %u bytes\n", *public_key_out_length); return false; } - memcpy(public_key_out, attestation_pubkey, sizeof(attestation_pubkey)); - *public_key_out_length = sizeof(attestation_pubkey); + size_t off = 0; + public_key_out[off++] = 0x04; + memcpy( + public_key_out + off, + G_endorsement_ctx.envelope.quote_auth_data->attestation_key.x, + sizeof(G_endorsement_ctx.envelope.quote_auth_data->attestation_key.x)); + off += + sizeof(G_endorsement_ctx.envelope.quote_auth_data->attestation_key.x); + memcpy( + public_key_out + off, + G_endorsement_ctx.envelope.quote_auth_data->attestation_key.y, + sizeof(G_endorsement_ctx.envelope.quote_auth_data->attestation_key.y)); + off += + sizeof(G_endorsement_ctx.envelope.quote_auth_data->attestation_key.y); + *public_key_out_length = off; + + // Sanity check + if (off != PUBKEY_UNCMP_LENGTH) { + LOG("Unexpected attestation public key length\n"); + return false; + } return true; -} \ No newline at end of file +} diff --git a/firmware/src/hal/sgx/src/trusted/hmac_sha256.c b/firmware/src/hal/sgx/src/trusted/hmac_sha256.c deleted file mode 120000 index f260ff64..00000000 --- a/firmware/src/hal/sgx/src/trusted/hmac_sha256.c +++ /dev/null @@ -1 +0,0 @@ -../../../x86/src/hmac_sha256.c \ No newline at end of file diff --git a/firmware/src/hal/sgx/src/trusted/hmac_sha256.h b/firmware/src/hal/sgx/src/trusted/hmac_sha256.h deleted file mode 120000 index 9fd6de87..00000000 --- a/firmware/src/hal/sgx/src/trusted/hmac_sha256.h +++ /dev/null @@ -1 +0,0 @@ -../../../x86/src/hmac_sha256.h \ No newline at end of file diff --git a/firmware/src/hal/sgx/src/trusted/platform.c b/firmware/src/hal/sgx/src/trusted/platform.c deleted file mode 120000 index ddeafb2f..00000000 --- a/firmware/src/hal/sgx/src/trusted/platform.c +++ /dev/null @@ -1 +0,0 @@ -../../../x86/src/platform.c \ No newline at end of file diff --git a/firmware/src/hal/sgx/src/trusted/platform.c b/firmware/src/hal/sgx/src/trusted/platform.c new file mode 100644 index 00000000..42f10dab --- /dev/null +++ b/firmware/src/hal/sgx/src/trusted/platform.c @@ -0,0 +1,48 @@ +/** + * 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. + */ + +#include "hal/platform.h" +#include "hal/log.h" + +#include + +void platform_memmove(void *dst, const void *src, unsigned int length) { + memmove(dst, src, length); +} + +void platform_request_exit() { + // Currently unsupported, just log the call + LOG("platform_request_exit called\n"); +} + +const char *platform_get_id() { + return "sgx"; +} + +uint64_t platform_get_timestamp() { + // Trusted way of getting current timestamp + // currently unsupported in OE/SGX, return zero + // for the time being + return (uint64_t)0; +} \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/der_utils/Makefile b/firmware/src/hal/sgx/test/der_utils/Makefile new file mode 100644 index 00000000..d9557131 --- /dev/null +++ b/firmware/src/hal/sgx/test/der_utils/Makefile @@ -0,0 +1,39 @@ +# 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. + +include ../common/common.mk + +PROG = test.out +OBJS = der_utils.o test_der_utils.o +CFLAGS += -I. + +all: $(PROG) + +$(PROG): $(OBJS) + $(CC) $(COVFLAGS) -o $@ $^ + +.PHONY: clean test +clean: + rm -f $(PROG) *.o $(COVFILES) + +test: all + ./$(PROG) diff --git a/firmware/src/hal/sgx/test/der_utils/test_der_utils.c b/firmware/src/hal/sgx/test/der_utils/test_der_utils.c new file mode 100644 index 00000000..493314a0 --- /dev/null +++ b/firmware/src/hal/sgx/test/der_utils/test_der_utils.c @@ -0,0 +1,148 @@ +/** + * 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. + */ + +#include +#include +#include +#include "der_utils.h" + +void test_der_encode(const sgx_ecdsa256_signature_t* sig, + const uint8_t* expected, + int expected_len) { + uint8_t dest[72]; // Buffer large enough for DER-encoded signature + int len = der_encode_signature(dest, (sgx_ecdsa256_signature_t*)sig); + + assert(len == expected_len); + assert(memcmp(dest, expected, expected_len) == 0); +} + +int main() { + printf("Test case 1: Standard values for R and S... "); + fflush(stdout); + sgx_ecdsa256_signature_t sig1 = { + .r = {0x1c, 0x1e, 0x24, 0xf5, 0x8d, 0x7b, 0xe5, 0xa0, 0xd3, 0x55, 0x4a, + 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, 0x78, 0xae, 0x92, 0xa0, 0xb4, + 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, 0x1f, 0x14, 0xa5}, + .s = {0x6f, 0x2a, 0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, 0x32, + 0xe9, 0xfa, 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, + 0xf9, 0x4c, 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8, 0x6c}}; + uint8_t expected1[] = { + 0x30, 0x44, 0x02, 0x20, 0x1c, 0x1e, 0x24, 0xf5, 0x8d, 0x7b, 0xe5, 0xa0, + 0xd3, 0x55, 0x4a, 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, 0x78, 0xae, 0x92, + 0xa0, 0xb4, 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, 0x1f, 0x14, 0xa5, + 0x02, 0x20, 0x6f, 0x2a, 0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, + 0x32, 0xe9, 0xfa, 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, + 0xf9, 0x4c, 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8, 0x6c}; + test_der_encode(&sig1, expected1, sizeof(expected1)); + printf("Passed.\n"); + + printf("Test case 2: High bit set in R... "); + fflush(stdout); + sgx_ecdsa256_signature_t sig2 = { + .r = {0x80, 0x00, 0x00, 0x01, 0x24, 0xf5, 0x8d, 0x7b, 0xe5, 0xa0, 0xd3, + 0x55, 0x4a, 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, 0x78, 0xae, 0x92, + 0xa0, 0xb4, 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, 0x1f}, + .s = {0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, 0x32, 0xe9, 0xfa, + 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, 0xf9, 0x4c, + 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8, 0x6c, 0x12, 0x34}}; + uint8_t expected2[] = { + 0x30, 0x45, 0x02, 0x21, 0x00, 0x80, 0x00, 0x00, 0x01, 0x24, 0xf5, 0x8d, + 0x7b, 0xe5, 0xa0, 0xd3, 0x55, 0x4a, 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, + 0x78, 0xae, 0x92, 0xa0, 0xb4, 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, + 0x1f, 0x02, 0x20, 0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, 0x32, + 0xe9, 0xfa, 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, 0xf9, + 0x4c, 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8, 0x6c, 0x12, 0x34}; + test_der_encode(&sig2, expected2, sizeof(expected2)); + printf("Passed.\n"); + + printf("Test case 3: Leading zeroes in R... "); + fflush(stdout); + sgx_ecdsa256_signature_t sig3 = { + .r = {0x00, 0x00, 0x00, 0x01, 0x24, 0xf5, 0x8d, 0x7b, 0xe5, 0xa0, 0xd3, + 0x55, 0x4a, 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, 0x78, 0xae, 0x92, + 0xa0, 0xb4, 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, 0x1f}, + .s = {0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, 0x32, 0xe9, 0xfa, + 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, 0xf9, 0x4c, + 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8, 0x6c, 0x12, 0x34}}; + uint8_t expected3[] = { + 0x30, 0x41, 0x02, 0x1d, 0x01, 0x24, 0xf5, 0x8d, 0x7b, 0xe5, 0xa0, 0xd3, + 0x55, 0x4a, 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, 0x78, 0xae, 0x92, 0xa0, + 0xb4, 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, 0x1f, 0x02, 0x20, 0x0f, + 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, 0x32, 0xe9, 0xfa, 0x28, 0xe7, + 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, 0xf9, 0x4c, 0x77, 0xb2, 0xf1, + 0xab, 0xf8, 0xd9, 0xf8, 0x6c, 0x12, 0x34}; + test_der_encode(&sig3, expected3, sizeof(expected3)); + printf("Passed.\n"); + + printf("Test case 4: Leading zeroes in S... "); + fflush(stdout); + sgx_ecdsa256_signature_t sig4 = { + .r = {0x1c, 0x1e, 0x24, 0xf5, 0x8d, 0x7b, 0xe5, 0xa0, 0xd3, 0x55, 0x4a, + 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, 0x78, 0xae, 0x92, 0xa0, 0xb4, + 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, 0x1f, 0x14, 0xa5}, + .s = {0x00, 0x00, 0x00, 0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, + 0x32, 0xe9, 0xfa, 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, + 0xf8, 0xf9, 0x4c, 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8}}; + uint8_t expected4[] = { + 0x30, 0x41, 0x02, 0x20, 0x1c, 0x1e, 0x24, 0xf5, 0x8d, 0x7b, 0xe5, 0xa0, + 0xd3, 0x55, 0x4a, 0x2f, 0x5f, 0x3e, 0x63, 0x3a, 0x8d, 0x78, 0xae, 0x92, + 0xa0, 0xb4, 0xa8, 0xc4, 0x95, 0xb1, 0xe3, 0x2d, 0x30, 0x1f, 0x14, 0xa5, + 0x02, 0x1d, 0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, 0x32, 0xe9, + 0xfa, 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, 0xf9, 0x4c, + 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8}; + test_der_encode(&sig4, expected4, sizeof(expected4)); + printf("Passed.\n"); + + printf("Test case 5: R is zero... "); + fflush(stdout); + sgx_ecdsa256_signature_t sig5 = { + .r = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + .s = {0x12, 0x34, 0x56, 0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, 0x0c, 0x95, + 0x32, 0xe9, 0xfa, 0x28, 0xe7, 0xbd, 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, + 0xf8, 0xf9, 0x4c, 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8}}; + uint8_t expected5[] = {0x30, 0x25, 0x02, 0x01, 0x00, 0x02, 0x20, 0x12, + 0x34, 0x56, 0x0f, 0x8b, 0x46, 0xd1, 0x27, 0x5c, + 0x0c, 0x95, 0x32, 0xe9, 0xfa, 0x28, 0xe7, 0xbd, + 0xf9, 0xcc, 0x7e, 0xf0, 0x2e, 0xf8, 0xf9, 0x4c, + 0x77, 0xb2, 0xf1, 0xab, 0xf8, 0xd9, 0xf8}; + test_der_encode(&sig5, expected5, sizeof(expected5)); + printf("Passed.\n"); + + printf("Test case 6: R and S are zero... "); + fflush(stdout); + sgx_ecdsa256_signature_t sig6 = { + .r = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + .s = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + uint8_t expected6[] = {0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00}; + test_der_encode(&sig6, expected6, sizeof(expected6)); + printf("Passed.\n"); + + return 0; +} \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/endorsement/Makefile b/firmware/src/hal/sgx/test/endorsement/Makefile new file mode 100644 index 00000000..86c19dd9 --- /dev/null +++ b/firmware/src/hal/sgx/test/endorsement/Makefile @@ -0,0 +1,39 @@ +# 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. + +include ../common/common.mk + +PROG = test.out +OBJS = endorsement.o test_endorsement.o mocks.o +CFLAGS += -I. + +all: $(PROG) + +$(PROG): $(OBJS) + $(CC) $(COVFLAGS) -o $@ $^ + +.PHONY: clean test +clean: + rm -f $(PROG) *.o $(COVFILES) + +test: all + ./$(PROG) diff --git a/firmware/src/hal/sgx/test/endorsement/mocks.c b/firmware/src/hal/sgx/test/endorsement/mocks.c new file mode 100644 index 00000000..98917046 --- /dev/null +++ b/firmware/src/hal/sgx/test/endorsement/mocks.c @@ -0,0 +1,137 @@ +/** + * 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. + */ + +#include +#include +#include +#include +#include "mocks.h" + +mock_config_t G_mock_config; + +#define MOCK_RESULT(fn) return G_mock_config.result_##fn ? OE_OK : OE_FAILURE + +uint8_t mock_format_id[] = {11, 22, 33}; +uint8_t mock_format_settings[] = {44, 55, 66, 77}; +uint8_t mock_evidence[] = MOCK_EVIDENCE; + +uint8_t der_encode_signature(uint8_t* dest, sgx_ecdsa256_signature_t* sig) { + memcpy(dest, sig->r, sizeof(sig->r)); + memcpy(dest + sizeof(sig->r), sig->s, sizeof(sig->s)); + return sizeof(sig->r) + sizeof(sig->s); +} + +oe_result_t oe_attester_initialize(void) { + MOCK_RESULT(oe_attester_initialize); +} + +oe_result_t oe_attester_select_format(const oe_uuid_t* format_ids, + size_t format_ids_length, + oe_uuid_t* selected_format_id) { + + const uint8_t expected_format_id[] = OE_FORMAT_UUID_SGX_ECDSA; + const oe_uuid_t assigned_format_id = {.b = {11, 22, 33}}; + + assert(format_ids_length == 1); + assert(!memcmp( + format_ids[0].b, expected_format_id, sizeof(expected_format_id))); + *selected_format_id = assigned_format_id; + MOCK_RESULT(oe_attester_select_format); +} + +oe_result_t oe_verifier_get_format_settings(const oe_uuid_t* format_id, + uint8_t** settings, + size_t* settings_size) { + + const uint8_t expected_format_id[] = {11, 22, 33}; + assert( + !memcmp(format_id->b, expected_format_id, sizeof(expected_format_id))); + assert(settings_size != NULL); + *settings = mock_format_settings; + *settings_size = sizeof(mock_format_settings); + + MOCK_RESULT(oe_verifier_get_format_settings); +} + +oe_result_t oe_get_evidence(const oe_uuid_t* format_id, + uint32_t flags, + const void* custom_claims_buffer, + size_t custom_claims_buffer_size, + const void* optional_parameters, + size_t optional_parameters_size, + uint8_t** evidence_buffer, + size_t* evidence_buffer_size, + uint8_t** endorsements_buffer, + size_t* endorsements_buffer_size) { + + // Test parameters + assert(flags == 0); + assert(!memcmp(format_id, mock_format_id, sizeof(mock_format_id))); + assert(!memcmp(optional_parameters, + mock_format_settings, + sizeof(mock_format_settings))); + assert(optional_parameters_size == sizeof(mock_format_settings)); + assert(endorsements_buffer == NULL); + assert(endorsements_buffer_size == NULL); + + // Mock evidence + size_t sz = G_mock_config.oe_get_evidence_buffer_size > 0 + ? G_mock_config.oe_get_evidence_buffer_size + : (sizeof(mock_evidence) + custom_claims_buffer_size); + G_mock_config.oe_get_evidence_buffer = malloc(sz); + if (G_mock_config.oe_get_evidence_buffer_size == 0) { + memcpy(G_mock_config.oe_get_evidence_buffer, + mock_evidence, + sizeof(mock_evidence)); + memcpy(G_mock_config.oe_get_evidence_buffer + sizeof(mock_evidence), + custom_claims_buffer, + custom_claims_buffer_size); + ((sgx_quote_t*)G_mock_config.oe_get_evidence_buffer)->signature_len = + sz - sizeof(sgx_quote_t) - custom_claims_buffer_size; + } + + // Result + *evidence_buffer = G_mock_config.oe_get_evidence_buffer; + *evidence_buffer_size = sz; + + MOCK_RESULT(oe_get_evidence); +} + +oe_result_t oe_free_evidence(uint8_t* evidence_buffer) { + G_mock_config.oe_get_evidence_buffer_freed |= + evidence_buffer == G_mock_config.oe_get_evidence_buffer; + MOCK_RESULT(oe_free_evidence); +} + +oe_result_t oe_attester_shutdown(void) { + MOCK_RESULT(oe_attester_shutdown); +} + +oe_result_t oe_verifier_initialize(void) { + MOCK_RESULT(oe_verifier_initialize); +} + +oe_result_t oe_verifier_shutdown(void) { + MOCK_RESULT(oe_verifier_shutdown); +} \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/endorsement/mocks.h b/firmware/src/hal/sgx/test/endorsement/mocks.h new file mode 100644 index 00000000..b1aa745f --- /dev/null +++ b/firmware/src/hal/sgx/test/endorsement/mocks.h @@ -0,0 +1,482 @@ +/** + * 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. + */ + +#ifndef __MOCKS_H +#define __MOCKS_H + +#include + +typedef struct { + bool result_oe_attester_initialize; + bool result_oe_attester_shutdown; + bool result_oe_verifier_initialize; + bool result_oe_verifier_shutdown; + bool result_oe_free_evidence; + bool result_oe_verifier_get_format_settings; + bool result_oe_get_evidence; + bool result_oe_attester_select_format; + + uint8_t* oe_get_evidence_buffer; + size_t oe_get_evidence_buffer_size; + bool oe_get_evidence_buffer_freed; +} mock_config_t; + +extern mock_config_t G_mock_config; + +#define MOCK_EVIDENCE \ + { \ + 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0f, \ + 0x00, 0x93, 0x9a, 0x72, 0x33, 0xf7, 0x9c, 0x4c, 0xa9, 0x94, 0x0a, \ + 0x0d, 0xb3, 0x95, 0x7f, 0x06, 0x07, 0xce, 0xae, 0x35, 0x49, 0xbc, \ + 0x72, 0x73, 0xeb, 0x34, 0xd5, 0x62, 0xf4, 0x56, 0x4f, 0xc1, 0x82, \ + 0x00, 0x00, 0x00, 0x00, 0x0e, 0x0e, 0x10, 0x0f, 0xff, 0xff, 0x01, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0xd3, 0x26, 0x88, 0xd3, 0xc1, 0xf3, 0xdf, 0xcc, 0x8b, \ + 0x0b, 0x36, 0xea, 0xc7, 0xc8, 0x9d, 0x49, 0xaf, 0x33, 0x18, 0x00, \ + 0xbd, 0x56, 0x24, 0x80, 0x44, 0x16, 0x6f, 0xa6, 0x69, 0x94, 0x42, \ + 0xc1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x71, 0x8c, 0x2f, 0x1a, 0x0e, 0xfb, 0xd5, 0x13, 0xe0, 0x16, 0xfa, \ + 0xfd, 0x6c, 0xf6, 0x2a, 0x62, 0x44, 0x42, 0xf2, 0xd8, 0x37, 0x08, \ + 0xd4, 0xb3, 0x3a, 0xb5, 0xa8, 0xd8, 0xc1, 0xcd, 0x4d, 0xd0, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x01, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x9e, 0x95, 0xbb, 0x87, 0x5c, 0x1a, \ + 0x72, 0x80, 0x71, 0xf7, 0x0a, 0xd8, 0xc9, 0xd0, 0x3f, 0x17, 0x44, \ + 0xc1, 0x9a, 0xcb, 0x05, 0x80, 0x92, 0x1e, 0x61, 0x1a, 0xc9, 0x10, \ + 0x4f, 0x77, 0x01, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x2b, 0x03, 0xa7, \ + 0xbd, 0x6b, 0x5d, 0xd9, 0xfe, 0xee, 0xb3, 0x75, 0xbd, 0x59, 0x77, \ + 0x30, 0xd2, 0x87, 0x26, 0x43, 0xb4, 0x7a, 0xff, 0x4d, 0xd6, 0x41, \ + 0xc5, 0xc3, 0xa2, 0xb8, 0x01, 0x6e, 0xbb, 0xd2, 0x27, 0xf6, 0x7e, \ + 0x7c, 0x23, 0xbb, 0xdd, 0xeb, 0x4f, 0x8f, 0xdd, 0xee, 0x03, 0x1a, \ + 0x2b, 0x96, 0x15, 0x01, 0xd1, 0xc2, 0x8d, 0xda, 0x08, 0x26, 0x69, \ + 0xd7, 0xac, 0x86, 0x1e, 0x6c, 0xa0, 0x24, 0xcb, 0x34, 0xc9, 0x0e, \ + 0xa6, 0xa8, 0xf9, 0xf2, 0x18, 0x1c, 0x90, 0x20, 0xcb, 0xcc, 0x7c, \ + 0x07, 0x3e, 0x69, 0x98, 0x17, 0x33, 0xc8, 0xde, 0xed, 0x6f, 0x6c, \ + 0x45, 0x18, 0x22, 0xaa, 0x08, 0x37, 0x63, 0x50, 0xff, 0x7d, 0xa0, \ + 0x1f, 0x84, 0x2b, 0xb4, 0x0c, 0x63, 0x1c, 0xbb, 0x71, 0x1f, 0x8b, \ + 0x6f, 0x7a, 0x4f, 0xae, 0x39, 0x83, 0x20, 0xa3, 0x88, 0x47, 0x74, \ + 0xd2, 0x50, 0xad, 0x0e, 0x0e, 0x10, 0x0f, 0xff, 0xff, 0x01, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x96, 0xb3, 0x47, 0xa6, 0x4e, 0x5a, 0x04, 0x5e, 0x27, 0x36, \ + 0x9c, 0x26, 0xe6, 0xdc, 0xda, 0x51, 0xfd, 0x7c, 0x85, 0x0e, 0x9b, \ + 0x3a, 0x3a, 0x79, 0xe7, 0x18, 0xf4, 0x32, 0x61, 0xde, 0xe1, 0xe4, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, \ + 0x4f, 0x57, 0x75, 0xd7, 0x96, 0x50, 0x3e, 0x96, 0x13, 0x7f, 0x77, \ + 0xc6, 0x8a, 0x82, 0x9a, 0x00, 0x56, 0xac, 0x8d, 0xed, 0x70, 0x14, \ + 0x0b, 0x08, 0x1b, 0x09, 0x44, 0x90, 0xc5, 0x7b, 0xff, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0a, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe7, 0x21, 0xd0, 0x32, 0x29, 0x54, \ + 0x82, 0x15, 0x89, 0x23, 0x7f, 0xd2, 0x7e, 0xfb, 0x8f, 0xef, 0x1a, \ + 0xcb, 0x3e, 0xcd, 0x6b, 0x03, 0x52, 0xc3, 0x12, 0x71, 0x55, 0x0f, \ + 0xc7, 0x0f, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x1f, 0x14, 0xd5, 0x32, 0x27, 0x4c, 0x43, 0x85, 0xfc, \ + 0x00, 0x19, 0xca, 0x2a, 0x21, 0xe5, 0x3e, 0x17, 0x14, 0x3c, 0xb6, \ + 0x23, 0x77, 0xca, 0x4f, 0xcd, 0xd9, 0x7f, 0xa9, 0xfe, 0xf8, 0xfb, \ + 0x25, 0x95, 0xd4, 0xee, 0x27, 0x2c, 0xf3, 0xc5, 0x12, 0xe3, 0x67, \ + 0x79, 0xde, 0x67, 0xdc, 0x78, 0x14, 0x98, 0x2f, 0x11, 0x60, 0xd9, \ + 0x81, 0xd1, 0x38, 0xa3, 0x2b, 0x26, 0x5e, 0x92, 0x8a, 0x05, 0x62, \ + 0x20, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, \ + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, \ + 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, \ + 0x1f, 0x05, 0x00, 0x62, 0x0e, 0x00, 0x00, 0x2d, 0x2d, 0x2d, 0x2d, \ + 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x43, 0x45, 0x52, 0x54, \ + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, \ + 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x45, 0x38, 0x7a, 0x43, 0x43, 0x42, \ + 0x4a, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, \ + 0x66, 0x72, 0x32, 0x64, 0x6c, 0x77, 0x4e, 0x34, 0x32, 0x44, 0x42, \ + 0x55, 0x41, 0x39, 0x43, 0x58, 0x49, 0x6b, 0x42, 0x6c, 0x47, 0x50, \ + 0x32, 0x76, 0x56, 0x33, 0x41, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4b, \ + 0x6f, 0x5a, 0x49, 0x7a, 0x6a, 0x30, 0x45, 0x41, 0x77, 0x49, 0x77, \ + 0x0a, 0x63, 0x44, 0x45, 0x69, 0x4d, 0x43, 0x41, 0x47, 0x41, 0x31, \ + 0x55, 0x45, 0x41, 0x77, 0x77, 0x5a, 0x53, 0x57, 0x35, 0x30, 0x5a, \ + 0x57, 0x77, 0x67, 0x55, 0x30, 0x64, 0x59, 0x49, 0x46, 0x42, 0x44, \ + 0x53, 0x79, 0x42, 0x51, 0x62, 0x47, 0x46, 0x30, 0x5a, 0x6d, 0x39, \ + 0x79, 0x62, 0x53, 0x42, 0x44, 0x51, 0x54, 0x45, 0x61, 0x4d, 0x42, \ + 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x52, 0x0a, \ + 0x53, 0x57, 0x35, 0x30, 0x5a, 0x57, 0x77, 0x67, 0x51, 0x32, 0x39, \ + 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, \ + 0x34, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4e, 0x56, 0x42, \ + 0x41, 0x63, 0x4d, 0x43, 0x31, 0x4e, 0x68, 0x62, 0x6e, 0x52, 0x68, \ + 0x49, 0x45, 0x4e, 0x73, 0x59, 0x58, 0x4a, 0x68, 0x4d, 0x51, 0x73, \ + 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x49, 0x0a, 0x44, \ + 0x41, 0x4a, 0x44, 0x51, 0x54, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, \ + 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4d, 0x43, 0x56, 0x56, 0x4d, \ + 0x77, 0x48, 0x68, 0x63, 0x4e, 0x4d, 0x6a, 0x51, 0x77, 0x4d, 0x7a, \ + 0x49, 0x7a, 0x4d, 0x44, 0x51, 0x30, 0x4e, 0x6a, 0x49, 0x78, 0x57, \ + 0x68, 0x63, 0x4e, 0x4d, 0x7a, 0x45, 0x77, 0x4d, 0x7a, 0x49, 0x7a, \ + 0x4d, 0x44, 0x51, 0x30, 0x4e, 0x6a, 0x49, 0x78, 0x0a, 0x57, 0x6a, \ + 0x42, 0x77, 0x4d, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, \ + 0x51, 0x51, 0x44, 0x44, 0x42, 0x6c, 0x4a, 0x62, 0x6e, 0x52, 0x6c, \ + 0x62, 0x43, 0x42, 0x54, 0x52, 0x31, 0x67, 0x67, 0x55, 0x45, 0x4e, \ + 0x4c, 0x49, 0x45, 0x4e, 0x6c, 0x63, 0x6e, 0x52, 0x70, 0x5a, 0x6d, \ + 0x6c, 0x6a, 0x59, 0x58, 0x52, 0x6c, 0x4d, 0x52, 0x6f, 0x77, 0x47, \ + 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4b, 0x0a, 0x44, 0x42, 0x46, \ + 0x4a, 0x62, 0x6e, 0x52, 0x6c, 0x62, 0x43, 0x42, 0x44, 0x62, 0x33, \ + 0x4a, 0x77, 0x62, 0x33, 0x4a, 0x68, 0x64, 0x47, 0x6c, 0x76, 0x62, \ + 0x6a, 0x45, 0x55, 0x4d, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, \ + 0x42, 0x77, 0x77, 0x4c, 0x55, 0x32, 0x46, 0x75, 0x64, 0x47, 0x45, \ + 0x67, 0x51, 0x32, 0x78, 0x68, 0x63, 0x6d, 0x45, 0x78, 0x43, 0x7a, \ + 0x41, 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x0a, 0x42, 0x41, 0x67, 0x4d, \ + 0x41, 0x6b, 0x4e, 0x42, 0x4d, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, \ + 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4a, 0x56, 0x55, 0x7a, \ + 0x42, 0x5a, 0x4d, 0x42, 0x4d, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, \ + 0x4d, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x43, 0x43, 0x71, 0x47, \ + 0x53, 0x4d, 0x34, 0x39, 0x41, 0x77, 0x45, 0x48, 0x41, 0x30, 0x49, \ + 0x41, 0x42, 0x4b, 0x6c, 0x37, 0x0a, 0x52, 0x44, 0x4e, 0x6c, 0x73, \ + 0x5a, 0x4b, 0x6b, 0x45, 0x74, 0x41, 0x63, 0x57, 0x37, 0x53, 0x66, \ + 0x43, 0x58, 0x31, 0x4a, 0x65, 0x67, 0x62, 0x76, 0x47, 0x71, 0x34, \ + 0x4f, 0x30, 0x72, 0x52, 0x55, 0x74, 0x30, 0x7a, 0x2f, 0x47, 0x36, \ + 0x66, 0x5a, 0x4a, 0x73, 0x4e, 0x6c, 0x70, 0x6d, 0x52, 0x77, 0x54, \ + 0x42, 0x34, 0x44, 0x59, 0x6b, 0x72, 0x67, 0x6b, 0x6d, 0x31, 0x74, \ + 0x2b, 0x39, 0x52, 0x70, 0x0a, 0x4c, 0x77, 0x78, 0x46, 0x58, 0x39, \ + 0x2f, 0x6b, 0x67, 0x68, 0x78, 0x69, 0x44, 0x51, 0x6d, 0x30, 0x6a, \ + 0x71, 0x6d, 0x6a, 0x67, 0x67, 0x4d, 0x4f, 0x4d, 0x49, 0x49, 0x44, \ + 0x43, 0x6a, 0x41, 0x66, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x53, 0x4d, \ + 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x53, 0x56, 0x62, 0x31, \ + 0x33, 0x4e, 0x76, 0x52, 0x76, 0x68, 0x36, 0x55, 0x42, 0x4a, 0x79, \ + 0x64, 0x54, 0x30, 0x0a, 0x4d, 0x38, 0x34, 0x42, 0x56, 0x77, 0x76, \ + 0x65, 0x56, 0x44, 0x42, 0x72, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x52, \ + 0x38, 0x45, 0x5a, 0x44, 0x42, 0x69, 0x4d, 0x47, 0x43, 0x67, 0x58, \ + 0x71, 0x42, 0x63, 0x68, 0x6c, 0x70, 0x6f, 0x64, 0x48, 0x52, 0x77, \ + 0x63, 0x7a, 0x6f, 0x76, 0x4c, 0x32, 0x46, 0x77, 0x61, 0x53, 0x35, \ + 0x30, 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x47, 0x56, 0x6b, 0x63, 0x32, \ + 0x56, 0x79, 0x0a, 0x64, 0x6d, 0x6c, 0x6a, 0x5a, 0x58, 0x4d, 0x75, \ + 0x61, 0x57, 0x35, 0x30, 0x5a, 0x57, 0x77, 0x75, 0x59, 0x32, 0x39, \ + 0x74, 0x4c, 0x33, 0x4e, 0x6e, 0x65, 0x43, 0x39, 0x6a, 0x5a, 0x58, \ + 0x4a, 0x30, 0x61, 0x57, 0x5a, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, \ + 0x57, 0x39, 0x75, 0x4c, 0x33, 0x59, 0x30, 0x4c, 0x33, 0x42, 0x6a, \ + 0x61, 0x32, 0x4e, 0x79, 0x62, 0x44, 0x39, 0x6a, 0x59, 0x54, 0x31, \ + 0x77, 0x0a, 0x62, 0x47, 0x46, 0x30, 0x5a, 0x6d, 0x39, 0x79, 0x62, \ + 0x53, 0x5a, 0x6c, 0x62, 0x6d, 0x4e, 0x76, 0x5a, 0x47, 0x6c, 0x75, \ + 0x5a, 0x7a, 0x31, 0x6b, 0x5a, 0x58, 0x49, 0x77, 0x48, 0x51, 0x59, \ + 0x44, 0x56, 0x52, 0x30, 0x4f, 0x42, 0x42, 0x59, 0x45, 0x46, 0x41, \ + 0x4c, 0x4b, 0x56, 0x35, 0x44, 0x46, 0x31, 0x36, 0x4b, 0x6e, 0x45, \ + 0x62, 0x53, 0x57, 0x35, 0x51, 0x4d, 0x39, 0x65, 0x63, 0x44, 0x71, \ + 0x0a, 0x42, 0x5a, 0x61, 0x48, 0x4d, 0x41, 0x34, 0x47, 0x41, 0x31, \ + 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2f, 0x77, 0x51, 0x45, 0x41, \ + 0x77, 0x49, 0x47, 0x77, 0x44, 0x41, 0x4d, 0x42, 0x67, 0x4e, 0x56, \ + 0x48, 0x52, 0x4d, 0x42, 0x41, 0x66, 0x38, 0x45, 0x41, 0x6a, 0x41, \ + 0x41, 0x4d, 0x49, 0x49, 0x43, 0x4f, 0x77, 0x59, 0x4a, 0x4b, 0x6f, \ + 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x0a, \ + 0x42, 0x49, 0x49, 0x43, 0x4c, 0x44, 0x43, 0x43, 0x41, 0x69, 0x67, \ + 0x77, 0x48, 0x67, 0x59, 0x4b, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, \ + 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, 0x51, 0x51, 0x51, 0x74, \ + 0x74, 0x4a, 0x58, 0x75, 0x69, 0x51, 0x56, 0x77, 0x71, 0x4d, 0x34, \ + 0x73, 0x37, 0x34, 0x67, 0x2b, 0x48, 0x78, 0x66, 0x4b, 0x54, 0x43, \ + 0x43, 0x41, 0x57, 0x55, 0x47, 0x43, 0x69, 0x71, 0x47, 0x0a, 0x53, \ + 0x49, 0x62, 0x34, 0x54, 0x51, 0x45, 0x4e, 0x41, 0x51, 0x49, 0x77, \ + 0x67, 0x67, 0x46, 0x56, 0x4d, 0x42, 0x41, 0x47, 0x43, 0x79, 0x71, \ + 0x47, 0x53, 0x49, 0x62, 0x34, 0x54, 0x51, 0x45, 0x4e, 0x41, 0x51, \ + 0x49, 0x42, 0x41, 0x67, 0x45, 0x4f, 0x4d, 0x42, 0x41, 0x47, 0x43, \ + 0x79, 0x71, 0x47, 0x53, 0x49, 0x62, 0x34, 0x54, 0x51, 0x45, 0x4e, \ + 0x41, 0x51, 0x49, 0x43, 0x41, 0x67, 0x45, 0x4f, 0x0a, 0x4d, 0x42, \ + 0x41, 0x47, 0x43, 0x79, 0x71, 0x47, 0x53, 0x49, 0x62, 0x34, 0x54, \ + 0x51, 0x45, 0x4e, 0x41, 0x51, 0x49, 0x44, 0x41, 0x67, 0x45, 0x44, \ + 0x4d, 0x42, 0x41, 0x47, 0x43, 0x79, 0x71, 0x47, 0x53, 0x49, 0x62, \ + 0x34, 0x54, 0x51, 0x45, 0x4e, 0x41, 0x51, 0x49, 0x45, 0x41, 0x67, \ + 0x45, 0x44, 0x4d, 0x42, 0x45, 0x47, 0x43, 0x79, 0x71, 0x47, 0x53, \ + 0x49, 0x62, 0x34, 0x54, 0x51, 0x45, 0x4e, 0x0a, 0x41, 0x51, 0x49, \ + 0x46, 0x41, 0x67, 0x49, 0x41, 0x2f, 0x7a, 0x41, 0x52, 0x42, 0x67, \ + 0x73, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x2b, 0x45, 0x30, 0x42, 0x44, \ + 0x51, 0x45, 0x43, 0x42, 0x67, 0x49, 0x43, 0x41, 0x50, 0x38, 0x77, \ + 0x45, 0x41, 0x59, 0x4c, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, \ + 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, 0x67, 0x63, 0x43, 0x41, 0x51, \ + 0x45, 0x77, 0x45, 0x41, 0x59, 0x4c, 0x0a, 0x4b, 0x6f, 0x5a, 0x49, \ + 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, 0x67, 0x67, \ + 0x43, 0x41, 0x51, 0x41, 0x77, 0x45, 0x41, 0x59, 0x4c, 0x4b, 0x6f, \ + 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, \ + 0x67, 0x6b, 0x43, 0x41, 0x51, 0x41, 0x77, 0x45, 0x41, 0x59, 0x4c, \ + 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, \ + 0x42, 0x41, 0x67, 0x6f, 0x43, 0x0a, 0x41, 0x51, 0x41, 0x77, 0x45, \ + 0x41, 0x59, 0x4c, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, \ + 0x41, 0x51, 0x30, 0x42, 0x41, 0x67, 0x73, 0x43, 0x41, 0x51, 0x41, \ + 0x77, 0x45, 0x41, 0x59, 0x4c, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, \ + 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, 0x67, 0x77, 0x43, 0x41, \ + 0x51, 0x41, 0x77, 0x45, 0x41, 0x59, 0x4c, 0x4b, 0x6f, 0x5a, 0x49, \ + 0x68, 0x76, 0x68, 0x4e, 0x0a, 0x41, 0x51, 0x30, 0x42, 0x41, 0x67, \ + 0x30, 0x43, 0x41, 0x51, 0x41, 0x77, 0x45, 0x41, 0x59, 0x4c, 0x4b, \ + 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, \ + 0x41, 0x67, 0x34, 0x43, 0x41, 0x51, 0x41, 0x77, 0x45, 0x41, 0x59, \ + 0x4c, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, \ + 0x30, 0x42, 0x41, 0x67, 0x38, 0x43, 0x41, 0x51, 0x41, 0x77, 0x45, \ + 0x41, 0x59, 0x4c, 0x0a, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, \ + 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, 0x68, 0x41, 0x43, 0x41, 0x51, \ + 0x41, 0x77, 0x45, 0x41, 0x59, 0x4c, 0x4b, 0x6f, 0x5a, 0x49, 0x68, \ + 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, 0x68, 0x45, 0x43, \ + 0x41, 0x51, 0x30, 0x77, 0x48, 0x77, 0x59, 0x4c, 0x4b, 0x6f, 0x5a, \ + 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, 0x68, \ + 0x49, 0x45, 0x0a, 0x45, 0x41, 0x34, 0x4f, 0x41, 0x77, 0x50, 0x2f, \ + 0x2f, 0x77, 0x45, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, \ + 0x41, 0x41, 0x41, 0x41, 0x77, 0x45, 0x41, 0x59, 0x4b, 0x4b, 0x6f, \ + 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x41, \ + 0x77, 0x51, 0x43, 0x41, 0x41, 0x41, 0x77, 0x46, 0x41, 0x59, 0x4b, \ + 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, \ + 0x42, 0x0a, 0x42, 0x41, 0x51, 0x47, 0x41, 0x47, 0x42, 0x71, 0x41, \ + 0x41, 0x41, 0x41, 0x4d, 0x41, 0x38, 0x47, 0x43, 0x69, 0x71, 0x47, \ + 0x53, 0x49, 0x62, 0x34, 0x54, 0x51, 0x45, 0x4e, 0x41, 0x51, 0x55, \ + 0x4b, 0x41, 0x51, 0x45, 0x77, 0x48, 0x67, 0x59, 0x4b, 0x4b, 0x6f, \ + 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x42, \ + 0x67, 0x51, 0x51, 0x44, 0x56, 0x65, 0x2f, 0x44, 0x58, 0x55, 0x56, \ + 0x0a, 0x45, 0x34, 0x67, 0x65, 0x6d, 0x74, 0x67, 0x4f, 0x35, 0x75, \ + 0x42, 0x70, 0x76, 0x44, 0x42, 0x45, 0x42, 0x67, 0x6f, 0x71, 0x68, \ + 0x6b, 0x69, 0x47, 0x2b, 0x45, 0x30, 0x42, 0x44, 0x51, 0x45, 0x48, \ + 0x4d, 0x44, 0x59, 0x77, 0x45, 0x41, 0x59, 0x4c, 0x4b, 0x6f, 0x5a, \ + 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, 0x42, 0x42, 0x77, \ + 0x45, 0x42, 0x41, 0x66, 0x38, 0x77, 0x45, 0x41, 0x59, 0x4c, 0x0a, \ + 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, 0x51, 0x30, \ + 0x42, 0x42, 0x77, 0x49, 0x42, 0x41, 0x51, 0x41, 0x77, 0x45, 0x41, \ + 0x59, 0x4c, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x68, 0x4e, 0x41, \ + 0x51, 0x30, 0x42, 0x42, 0x77, 0x4d, 0x42, 0x41, 0x51, 0x41, 0x77, \ + 0x43, 0x67, 0x59, 0x49, 0x4b, 0x6f, 0x5a, 0x49, 0x7a, 0x6a, 0x30, \ + 0x45, 0x41, 0x77, 0x49, 0x44, 0x53, 0x51, 0x41, 0x77, 0x0a, 0x52, \ + 0x67, 0x49, 0x68, 0x41, 0x4a, 0x46, 0x67, 0x66, 0x37, 0x38, 0x48, \ + 0x67, 0x67, 0x54, 0x42, 0x74, 0x76, 0x51, 0x50, 0x58, 0x5a, 0x4a, \ + 0x78, 0x2f, 0x33, 0x46, 0x6d, 0x37, 0x31, 0x76, 0x43, 0x4f, 0x6d, \ + 0x74, 0x38, 0x32, 0x70, 0x63, 0x65, 0x39, 0x31, 0x4d, 0x32, 0x5a, \ + 0x41, 0x49, 0x30, 0x41, 0x69, 0x45, 0x41, 0x69, 0x5a, 0x4d, 0x50, \ + 0x42, 0x62, 0x5a, 0x5a, 0x6d, 0x76, 0x52, 0x32, 0x0a, 0x76, 0x2b, \ + 0x31, 0x6d, 0x72, 0x73, 0x37, 0x36, 0x4a, 0x65, 0x67, 0x6c, 0x44, \ + 0x51, 0x2b, 0x70, 0x4b, 0x2f, 0x53, 0x4c, 0x4e, 0x39, 0x34, 0x6c, \ + 0x34, 0x2b, 0x6a, 0x4d, 0x35, 0x44, 0x41, 0x3d, 0x0a, 0x2d, 0x2d, \ + 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, \ + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, \ + 0x2d, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, \ + 0x4e, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, \ + 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, \ + 0x43, 0x6c, 0x6a, 0x43, 0x43, 0x41, 0x6a, 0x32, 0x67, 0x41, 0x77, \ + 0x49, 0x42, 0x41, 0x67, 0x49, 0x56, 0x41, 0x4a, 0x56, 0x76, 0x58, \ + 0x63, 0x32, 0x39, 0x47, 0x2b, 0x48, 0x70, 0x51, 0x45, 0x6e, 0x4a, \ + 0x31, 0x50, 0x51, 0x7a, 0x7a, 0x67, 0x46, 0x58, 0x43, 0x39, 0x35, \ + 0x55, 0x4d, 0x41, 0x6f, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4d, \ + 0x34, 0x39, 0x42, 0x41, 0x4d, 0x43, 0x0a, 0x4d, 0x47, 0x67, 0x78, \ + 0x47, 0x6a, 0x41, 0x59, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x4d, \ + 0x4d, 0x45, 0x55, 0x6c, 0x75, 0x64, 0x47, 0x56, 0x73, 0x49, 0x46, \ + 0x4e, 0x48, 0x57, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, \ + 0x45, 0x4e, 0x42, 0x4d, 0x52, 0x6f, 0x77, 0x47, 0x41, 0x59, 0x44, \ + 0x56, 0x51, 0x51, 0x4b, 0x44, 0x42, 0x46, 0x4a, 0x62, 0x6e, 0x52, \ + 0x6c, 0x62, 0x43, 0x42, 0x44, 0x0a, 0x62, 0x33, 0x4a, 0x77, 0x62, \ + 0x33, 0x4a, 0x68, 0x64, 0x47, 0x6c, 0x76, 0x62, 0x6a, 0x45, 0x55, \ + 0x4d, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x77, 0x77, \ + 0x4c, 0x55, 0x32, 0x46, 0x75, 0x64, 0x47, 0x45, 0x67, 0x51, 0x32, \ + 0x78, 0x68, 0x63, 0x6d, 0x45, 0x78, 0x43, 0x7a, 0x41, 0x4a, 0x42, \ + 0x67, 0x4e, 0x56, 0x42, 0x41, 0x67, 0x4d, 0x41, 0x6b, 0x4e, 0x42, \ + 0x4d, 0x51, 0x73, 0x77, 0x0a, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, \ + 0x51, 0x47, 0x45, 0x77, 0x4a, 0x56, 0x55, 0x7a, 0x41, 0x65, 0x46, \ + 0x77, 0x30, 0x78, 0x4f, 0x44, 0x41, 0x31, 0x4d, 0x6a, 0x45, 0x78, \ + 0x4d, 0x44, 0x55, 0x77, 0x4d, 0x54, 0x42, 0x61, 0x46, 0x77, 0x30, \ + 0x7a, 0x4d, 0x7a, 0x41, 0x31, 0x4d, 0x6a, 0x45, 0x78, 0x4d, 0x44, \ + 0x55, 0x77, 0x4d, 0x54, 0x42, 0x61, 0x4d, 0x48, 0x41, 0x78, 0x49, \ + 0x6a, 0x41, 0x67, 0x0a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x4d, \ + 0x4d, 0x47, 0x55, 0x6c, 0x75, 0x64, 0x47, 0x56, 0x73, 0x49, 0x46, \ + 0x4e, 0x48, 0x57, 0x43, 0x42, 0x51, 0x51, 0x30, 0x73, 0x67, 0x55, \ + 0x47, 0x78, 0x68, 0x64, 0x47, 0x5a, 0x76, 0x63, 0x6d, 0x30, 0x67, \ + 0x51, 0x30, 0x45, 0x78, 0x47, 0x6a, 0x41, 0x59, 0x42, 0x67, 0x4e, \ + 0x56, 0x42, 0x41, 0x6f, 0x4d, 0x45, 0x55, 0x6c, 0x75, 0x64, 0x47, \ + 0x56, 0x73, 0x0a, 0x49, 0x45, 0x4e, 0x76, 0x63, 0x6e, 0x42, 0x76, \ + 0x63, 0x6d, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4d, 0x52, 0x51, \ + 0x77, 0x45, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, \ + 0x74, 0x54, 0x59, 0x57, 0x35, 0x30, 0x59, 0x53, 0x42, 0x44, 0x62, \ + 0x47, 0x46, 0x79, 0x59, 0x54, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, \ + 0x41, 0x31, 0x55, 0x45, 0x43, 0x41, 0x77, 0x43, 0x51, 0x30, 0x45, \ + 0x78, 0x0a, 0x43, 0x7a, 0x41, 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x42, \ + 0x41, 0x59, 0x54, 0x41, 0x6c, 0x56, 0x54, 0x4d, 0x46, 0x6b, 0x77, \ + 0x45, 0x77, 0x59, 0x48, 0x4b, 0x6f, 0x5a, 0x49, 0x7a, 0x6a, 0x30, \ + 0x43, 0x41, 0x51, 0x59, 0x49, 0x4b, 0x6f, 0x5a, 0x49, 0x7a, 0x6a, \ + 0x30, 0x44, 0x41, 0x51, 0x63, 0x44, 0x51, 0x67, 0x41, 0x45, 0x4e, \ + 0x53, 0x42, 0x2f, 0x37, 0x74, 0x32, 0x31, 0x6c, 0x58, 0x53, 0x4f, \ + 0x0a, 0x32, 0x43, 0x75, 0x7a, 0x70, 0x78, 0x77, 0x37, 0x34, 0x65, \ + 0x4a, 0x42, 0x37, 0x32, 0x45, 0x79, 0x44, 0x47, 0x67, 0x57, 0x35, \ + 0x72, 0x58, 0x43, 0x74, 0x78, 0x32, 0x74, 0x56, 0x54, 0x4c, 0x71, \ + 0x36, 0x68, 0x4b, 0x6b, 0x36, 0x7a, 0x2b, 0x55, 0x69, 0x52, 0x5a, \ + 0x43, 0x6e, 0x71, 0x52, 0x37, 0x70, 0x73, 0x4f, 0x76, 0x67, 0x71, \ + 0x46, 0x65, 0x53, 0x78, 0x6c, 0x6d, 0x54, 0x6c, 0x4a, 0x6c, 0x0a, \ + 0x65, 0x54, 0x6d, 0x69, 0x32, 0x57, 0x59, 0x7a, 0x33, 0x71, 0x4f, \ + 0x42, 0x75, 0x7a, 0x43, 0x42, 0x75, 0x44, 0x41, 0x66, 0x42, 0x67, \ + 0x4e, 0x56, 0x48, 0x53, 0x4d, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, \ + 0x42, 0x51, 0x69, 0x5a, 0x51, 0x7a, 0x57, 0x57, 0x70, 0x30, 0x30, \ + 0x69, 0x66, 0x4f, 0x44, 0x74, 0x4a, 0x56, 0x53, 0x76, 0x31, 0x41, \ + 0x62, 0x4f, 0x53, 0x63, 0x47, 0x72, 0x44, 0x42, 0x53, 0x0a, 0x42, \ + 0x67, 0x4e, 0x56, 0x48, 0x52, 0x38, 0x45, 0x53, 0x7a, 0x42, 0x4a, \ + 0x4d, 0x45, 0x65, 0x67, 0x52, 0x61, 0x42, 0x44, 0x68, 0x6b, 0x46, \ + 0x6f, 0x64, 0x48, 0x52, 0x77, 0x63, 0x7a, 0x6f, 0x76, 0x4c, 0x32, \ + 0x4e, 0x6c, 0x63, 0x6e, 0x52, 0x70, 0x5a, 0x6d, 0x6c, 0x6a, 0x59, \ + 0x58, 0x52, 0x6c, 0x63, 0x79, 0x35, 0x30, 0x63, 0x6e, 0x56, 0x7a, \ + 0x64, 0x47, 0x56, 0x6b, 0x63, 0x32, 0x56, 0x79, 0x0a, 0x64, 0x6d, \ + 0x6c, 0x6a, 0x5a, 0x58, 0x4d, 0x75, 0x61, 0x57, 0x35, 0x30, 0x5a, \ + 0x57, 0x77, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4c, 0x30, 0x6c, 0x75, \ + 0x64, 0x47, 0x56, 0x73, 0x55, 0x30, 0x64, 0x59, 0x55, 0x6d, 0x39, \ + 0x76, 0x64, 0x45, 0x4e, 0x42, 0x4c, 0x6d, 0x52, 0x6c, 0x63, 0x6a, \ + 0x41, 0x64, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, \ + 0x67, 0x51, 0x55, 0x6c, 0x57, 0x39, 0x64, 0x0a, 0x7a, 0x62, 0x30, \ + 0x62, 0x34, 0x65, 0x6c, 0x41, 0x53, 0x63, 0x6e, 0x55, 0x39, 0x44, \ + 0x50, 0x4f, 0x41, 0x56, 0x63, 0x4c, 0x33, 0x6c, 0x51, 0x77, 0x44, \ + 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2f, \ + 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4d, 0x42, 0x49, \ + 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2f, 0x77, \ + 0x51, 0x49, 0x4d, 0x41, 0x59, 0x42, 0x0a, 0x41, 0x66, 0x38, 0x43, \ + 0x41, 0x51, 0x41, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4b, 0x6f, 0x5a, \ + 0x49, 0x7a, 0x6a, 0x30, 0x45, 0x41, 0x77, 0x49, 0x44, 0x52, 0x77, \ + 0x41, 0x77, 0x52, 0x41, 0x49, 0x67, 0x58, 0x73, 0x56, 0x6b, 0x69, \ + 0x30, 0x77, 0x2b, 0x69, 0x36, 0x56, 0x59, 0x47, 0x57, 0x33, 0x55, \ + 0x46, 0x2f, 0x32, 0x32, 0x75, 0x61, 0x58, 0x65, 0x30, 0x59, 0x4a, \ + 0x44, 0x6a, 0x31, 0x55, 0x65, 0x0a, 0x6e, 0x41, 0x2b, 0x54, 0x6a, \ + 0x44, 0x31, 0x61, 0x69, 0x35, 0x63, 0x43, 0x49, 0x43, 0x59, 0x62, \ + 0x31, 0x53, 0x41, 0x6d, 0x44, 0x35, 0x78, 0x6b, 0x66, 0x54, 0x56, \ + 0x70, 0x76, 0x6f, 0x34, 0x55, 0x6f, 0x79, 0x69, 0x53, 0x59, 0x78, \ + 0x72, 0x44, 0x57, 0x4c, 0x6d, 0x55, 0x52, 0x34, 0x43, 0x49, 0x39, \ + 0x4e, 0x4b, 0x79, 0x66, 0x50, 0x4e, 0x2b, 0x0a, 0x2d, 0x2d, 0x2d, \ + 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, \ + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, \ + 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, \ + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, \ + 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x43, \ + 0x6a, 0x7a, 0x43, 0x43, 0x41, 0x6a, 0x53, 0x67, 0x41, 0x77, 0x49, \ + 0x42, 0x41, 0x67, 0x49, 0x55, 0x49, 0x6d, 0x55, 0x4d, 0x31, 0x6c, \ + 0x71, 0x64, 0x4e, 0x49, 0x6e, 0x7a, 0x67, 0x37, 0x53, 0x56, 0x55, \ + 0x72, 0x39, 0x51, 0x47, 0x7a, 0x6b, 0x6e, 0x42, 0x71, 0x77, 0x77, \ + 0x43, 0x67, 0x59, 0x49, 0x4b, 0x6f, 0x5a, 0x49, 0x7a, 0x6a, 0x30, \ + 0x45, 0x41, 0x77, 0x49, 0x77, 0x0a, 0x61, 0x44, 0x45, 0x61, 0x4d, \ + 0x42, 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x52, \ + 0x53, 0x57, 0x35, 0x30, 0x5a, 0x57, 0x77, 0x67, 0x55, 0x30, 0x64, \ + 0x59, 0x49, 0x46, 0x4a, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, \ + 0x45, 0x78, 0x47, 0x6a, 0x41, 0x59, 0x42, 0x67, 0x4e, 0x56, 0x42, \ + 0x41, 0x6f, 0x4d, 0x45, 0x55, 0x6c, 0x75, 0x64, 0x47, 0x56, 0x73, \ + 0x49, 0x45, 0x4e, 0x76, 0x0a, 0x63, 0x6e, 0x42, 0x76, 0x63, 0x6d, \ + 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4d, 0x52, 0x51, 0x77, 0x45, \ + 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, 0x74, 0x54, \ + 0x59, 0x57, 0x35, 0x30, 0x59, 0x53, 0x42, 0x44, 0x62, 0x47, 0x46, \ + 0x79, 0x59, 0x54, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, 0x41, 0x31, \ + 0x55, 0x45, 0x43, 0x41, 0x77, 0x43, 0x51, 0x30, 0x45, 0x78, 0x43, \ + 0x7a, 0x41, 0x4a, 0x0a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x59, \ + 0x54, 0x41, 0x6c, 0x56, 0x54, 0x4d, 0x42, 0x34, 0x58, 0x44, 0x54, \ + 0x45, 0x34, 0x4d, 0x44, 0x55, 0x79, 0x4d, 0x54, 0x45, 0x77, 0x4e, \ + 0x44, 0x55, 0x78, 0x4d, 0x46, 0x6f, 0x58, 0x44, 0x54, 0x51, 0x35, \ + 0x4d, 0x54, 0x49, 0x7a, 0x4d, 0x54, 0x49, 0x7a, 0x4e, 0x54, 0x6b, \ + 0x31, 0x4f, 0x56, 0x6f, 0x77, 0x61, 0x44, 0x45, 0x61, 0x4d, 0x42, \ + 0x67, 0x47, 0x0a, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x52, \ + 0x53, 0x57, 0x35, 0x30, 0x5a, 0x57, 0x77, 0x67, 0x55, 0x30, 0x64, \ + 0x59, 0x49, 0x46, 0x4a, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, \ + 0x45, 0x78, 0x47, 0x6a, 0x41, 0x59, 0x42, 0x67, 0x4e, 0x56, 0x42, \ + 0x41, 0x6f, 0x4d, 0x45, 0x55, 0x6c, 0x75, 0x64, 0x47, 0x56, 0x73, \ + 0x49, 0x45, 0x4e, 0x76, 0x63, 0x6e, 0x42, 0x76, 0x63, 0x6d, 0x46, \ + 0x30, 0x0a, 0x61, 0x57, 0x39, 0x75, 0x4d, 0x52, 0x51, 0x77, 0x45, \ + 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, 0x74, 0x54, \ + 0x59, 0x57, 0x35, 0x30, 0x59, 0x53, 0x42, 0x44, 0x62, 0x47, 0x46, \ + 0x79, 0x59, 0x54, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, 0x41, 0x31, \ + 0x55, 0x45, 0x43, 0x41, 0x77, 0x43, 0x51, 0x30, 0x45, 0x78, 0x43, \ + 0x7a, 0x41, 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x59, 0x54, \ + 0x0a, 0x41, 0x6c, 0x56, 0x54, 0x4d, 0x46, 0x6b, 0x77, 0x45, 0x77, \ + 0x59, 0x48, 0x4b, 0x6f, 0x5a, 0x49, 0x7a, 0x6a, 0x30, 0x43, 0x41, \ + 0x51, 0x59, 0x49, 0x4b, 0x6f, 0x5a, 0x49, 0x7a, 0x6a, 0x30, 0x44, \ + 0x41, 0x51, 0x63, 0x44, 0x51, 0x67, 0x41, 0x45, 0x43, 0x36, 0x6e, \ + 0x45, 0x77, 0x4d, 0x44, 0x49, 0x59, 0x5a, 0x4f, 0x6a, 0x2f, 0x69, \ + 0x50, 0x57, 0x73, 0x43, 0x7a, 0x61, 0x45, 0x4b, 0x69, 0x37, 0x0a, \ + 0x31, 0x4f, 0x69, 0x4f, 0x53, 0x4c, 0x52, 0x46, 0x68, 0x57, 0x47, \ + 0x6a, 0x62, 0x6e, 0x42, 0x56, 0x4a, 0x66, 0x56, 0x6e, 0x6b, 0x59, \ + 0x34, 0x75, 0x33, 0x49, 0x6a, 0x6b, 0x44, 0x59, 0x59, 0x4c, 0x30, \ + 0x4d, 0x78, 0x4f, 0x34, 0x6d, 0x71, 0x73, 0x79, 0x59, 0x6a, 0x6c, \ + 0x42, 0x61, 0x6c, 0x54, 0x56, 0x59, 0x78, 0x46, 0x50, 0x32, 0x73, \ + 0x4a, 0x42, 0x4b, 0x35, 0x7a, 0x6c, 0x4b, 0x4f, 0x42, 0x0a, 0x75, \ + 0x7a, 0x43, 0x42, 0x75, 0x44, 0x41, 0x66, 0x42, 0x67, 0x4e, 0x56, \ + 0x48, 0x53, 0x4d, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x51, \ + 0x69, 0x5a, 0x51, 0x7a, 0x57, 0x57, 0x70, 0x30, 0x30, 0x69, 0x66, \ + 0x4f, 0x44, 0x74, 0x4a, 0x56, 0x53, 0x76, 0x31, 0x41, 0x62, 0x4f, \ + 0x53, 0x63, 0x47, 0x72, 0x44, 0x42, 0x53, 0x42, 0x67, 0x4e, 0x56, \ + 0x48, 0x52, 0x38, 0x45, 0x53, 0x7a, 0x42, 0x4a, 0x0a, 0x4d, 0x45, \ + 0x65, 0x67, 0x52, 0x61, 0x42, 0x44, 0x68, 0x6b, 0x46, 0x6f, 0x64, \ + 0x48, 0x52, 0x77, 0x63, 0x7a, 0x6f, 0x76, 0x4c, 0x32, 0x4e, 0x6c, \ + 0x63, 0x6e, 0x52, 0x70, 0x5a, 0x6d, 0x6c, 0x6a, 0x59, 0x58, 0x52, \ + 0x6c, 0x63, 0x79, 0x35, 0x30, 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x47, \ + 0x56, 0x6b, 0x63, 0x32, 0x56, 0x79, 0x64, 0x6d, 0x6c, 0x6a, 0x5a, \ + 0x58, 0x4d, 0x75, 0x61, 0x57, 0x35, 0x30, 0x0a, 0x5a, 0x57, 0x77, \ + 0x75, 0x59, 0x32, 0x39, 0x74, 0x4c, 0x30, 0x6c, 0x75, 0x64, 0x47, \ + 0x56, 0x73, 0x55, 0x30, 0x64, 0x59, 0x55, 0x6d, 0x39, 0x76, 0x64, \ + 0x45, 0x4e, 0x42, 0x4c, 0x6d, 0x52, 0x6c, 0x63, 0x6a, 0x41, 0x64, \ + 0x42, 0x67, 0x4e, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, \ + 0x55, 0x49, 0x6d, 0x55, 0x4d, 0x31, 0x6c, 0x71, 0x64, 0x4e, 0x49, \ + 0x6e, 0x7a, 0x67, 0x37, 0x53, 0x56, 0x0a, 0x55, 0x72, 0x39, 0x51, \ + 0x47, 0x7a, 0x6b, 0x6e, 0x42, 0x71, 0x77, 0x77, 0x44, 0x67, 0x59, \ + 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2f, 0x42, 0x41, \ + 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4d, 0x42, 0x49, 0x47, 0x41, \ + 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2f, 0x77, 0x51, 0x49, \ + 0x4d, 0x41, 0x59, 0x42, 0x41, 0x66, 0x38, 0x43, 0x41, 0x51, 0x45, \ + 0x77, 0x43, 0x67, 0x59, 0x49, 0x0a, 0x4b, 0x6f, 0x5a, 0x49, 0x7a, \ + 0x6a, 0x30, 0x45, 0x41, 0x77, 0x49, 0x44, 0x53, 0x51, 0x41, 0x77, \ + 0x52, 0x67, 0x49, 0x68, 0x41, 0x4f, 0x57, 0x2f, 0x35, 0x51, 0x6b, \ + 0x52, 0x2b, 0x53, 0x39, 0x43, 0x69, 0x53, 0x44, 0x63, 0x4e, 0x6f, \ + 0x6f, 0x77, 0x4c, 0x75, 0x50, 0x52, 0x4c, 0x73, 0x57, 0x47, 0x66, \ + 0x2f, 0x59, 0x69, 0x37, 0x47, 0x53, 0x58, 0x39, 0x34, 0x42, 0x67, \ + 0x77, 0x54, 0x77, 0x67, 0x0a, 0x41, 0x69, 0x45, 0x41, 0x34, 0x4a, \ + 0x30, 0x6c, 0x72, 0x48, 0x6f, 0x4d, 0x73, 0x2b, 0x58, 0x6f, 0x35, \ + 0x6f, 0x2f, 0x73, 0x58, 0x36, 0x4f, 0x39, 0x51, 0x57, 0x78, 0x48, \ + 0x52, 0x41, 0x76, 0x5a, 0x55, 0x47, 0x4f, 0x64, 0x52, 0x51, 0x37, \ + 0x63, 0x76, 0x71, 0x52, 0x58, 0x61, 0x71, 0x49, 0x3d, 0x0a, 0x2d, \ + 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, \ + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, \ + 0x2d, 0x2d, 0x0a, 0x00 \ + } + +#endif // __MOCKS_H diff --git a/firmware/src/hal/sgx/test/endorsement/test_endorsement.c b/firmware/src/hal/sgx/test/endorsement/test_endorsement.c new file mode 100644 index 00000000..ce73f2a1 --- /dev/null +++ b/firmware/src/hal/sgx/test/endorsement/test_endorsement.c @@ -0,0 +1,299 @@ +/** + * 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. + */ + +#include +#include +#include +#include +#include +#include + +#include "hal/endorsement.h" +#include "hal/constants.h" + +#include +#include "mocks.h" + +uint8_t msg[] = "this is a message"; +uint8_t signature[MAX_SIGNATURE_LENGTH]; +uint8_t signature_length; +extern uint8_t mock_evidence[]; + +void setup_no_init() { + // This has the side effect of clearing the initialized state of the + // endorsement module + G_mock_config.result_oe_attester_initialize = false; + endorsement_init(); + + G_mock_config.result_oe_attester_initialize = true; + G_mock_config.result_oe_verifier_initialize = true; + G_mock_config.result_oe_attester_select_format = true; + G_mock_config.result_oe_verifier_get_format_settings = true; + G_mock_config.result_oe_get_evidence = true; + G_mock_config.oe_get_evidence_buffer_freed = false; + + signature_length = sizeof(signature); + if (G_mock_config.oe_get_evidence_buffer != NULL) { + free(G_mock_config.oe_get_evidence_buffer); + G_mock_config.oe_get_evidence_buffer = NULL; + } + G_mock_config.oe_get_evidence_buffer_size = 0; +} + +void setup() { + setup_no_init(); + assert(endorsement_init()); +} + +void setup_and_sign() { + setup(); + assert(endorsement_sign(msg, sizeof(msg), signature, &signature_length)); + assert(G_mock_config.oe_get_evidence_buffer_freed); +} + +void test_endorsement_init_ok() { + setup_no_init(); + assert(endorsement_init() == true); +} + +void test_endorsement_init_err_attinit() { + setup_no_init(); + G_mock_config.result_oe_attester_initialize = false; + assert(endorsement_init() == false); +} + +void test_endorsement_init_err_verinit() { + setup_no_init(); + G_mock_config.result_oe_verifier_initialize = false; + assert(endorsement_init() == false); +} + +void test_endorsement_init_err_selfmt() { + setup_no_init(); + G_mock_config.result_oe_attester_select_format = false; + assert(endorsement_init() == false); +} + +void test_endorsement_init_err_getfmt() { + setup_no_init(); + G_mock_config.result_oe_verifier_get_format_settings = false; + assert(endorsement_init() == false); +} + +void test_signature_ok() { + setup(); + + assert(endorsement_sign(msg, sizeof(msg), signature, &signature_length)); + + sgx_ecdsa256_signature_t* sig = + &((sgx_quote_auth_data_t*)(mock_evidence + sizeof(sgx_quote_t))) + ->signature; + assert(!memcmp(signature, sig->r, sizeof(sig->r))); + assert(!memcmp(signature + sizeof(sig->r), sig->s, sizeof(sig->s))); + assert(signature_length == sizeof(sig->r) + sizeof(sig->s)); + + assert(G_mock_config.oe_get_evidence_buffer_freed); +} + +void test_signature_err_notinit() { + setup_no_init(); + + assert(!endorsement_sign(msg, sizeof(msg), signature, &signature_length)); +} + +void test_signature_err_sigbuftoosmall() { + setup(); + + signature_length = 10; + assert(!endorsement_sign(msg, sizeof(msg), signature, &signature_length)); + + assert(!G_mock_config.oe_get_evidence_buffer_freed); +} + +void test_signature_err_evibuftoobig() { + setup(); + + G_mock_config.oe_get_evidence_buffer_size = 100000; + assert(!endorsement_sign(msg, sizeof(msg), signature, &signature_length)); + + assert(G_mock_config.oe_get_evidence_buffer_freed); +} + +void test_signature_err_evidencebroken() { + setup(); + + G_mock_config.oe_get_evidence_buffer_size = 1000; + assert(!endorsement_sign(msg, sizeof(msg), signature, &signature_length)); + + assert(G_mock_config.oe_get_evidence_buffer_freed); +} + +void test_get_envelope_ok() { + setup_and_sign(); + assert(!memcmp(endorsement_get_envelope(), + mock_evidence, + sizeof(sgx_quote_t) - sizeof(uint32_t))); + assert(!memcmp(endorsement_get_envelope() + sizeof(sgx_quote_t), + mock_evidence + sizeof(sgx_quote_t), + endorsement_get_envelope_length() - sizeof(sgx_quote_t) - + sizeof(msg))); + assert(!memcmp(endorsement_get_envelope() + + endorsement_get_envelope_length() - sizeof(msg), + msg, + sizeof(msg))); +} + +void test_get_envelope_nosignature() { + setup(); + assert(endorsement_get_envelope() == NULL); + assert(endorsement_get_envelope_length() == 0); +} + +void test_get_code_hash_ok() { + setup_and_sign(); + + uint8_t expected_code_hash[] = { + 0xd3, 0x26, 0x88, 0xd3, 0xc1, 0xf3, 0xdf, 0xcc, 0x8b, 0x0b, 0x36, + 0xea, 0xc7, 0xc8, 0x9d, 0x49, 0xaf, 0x33, 0x18, 0x00, 0xbd, 0x56, + 0x24, 0x80, 0x44, 0x16, 0x6f, 0xa6, 0x69, 0x94, 0x42, 0xc1}; + + uint8_t code_hash[sizeof(expected_code_hash) + 10]; + uint8_t code_hash_length = sizeof(code_hash); + + assert(endorsement_get_code_hash(code_hash, &code_hash_length)); + assert(code_hash_length == sizeof(expected_code_hash)); + assert(!memcmp(code_hash, expected_code_hash, sizeof(expected_code_hash))); +} + +void test_get_code_hash_err_nullbuf() { + setup_and_sign(); + + uint8_t* code_hash = NULL; + uint8_t code_hash_length = 100; + + assert(!endorsement_get_code_hash(code_hash, &code_hash_length)); + assert(code_hash_length == 100); +} + +void test_get_code_hash_err_nosignature() { + setup(); + + uint8_t code_hash[123]; + uint8_t code_hash_length = sizeof(code_hash); + + assert(!endorsement_get_code_hash(code_hash, &code_hash_length)); + assert(code_hash_length == sizeof(code_hash)); +} + +void test_get_code_hash_err_buftoosmall() { + setup_and_sign(); + + uint8_t code_hash[10]; + uint8_t code_hash_length = sizeof(code_hash); + + assert(!endorsement_get_code_hash(code_hash, &code_hash_length)); + assert(code_hash_length == sizeof(code_hash)); +} + +void test_get_public_key_ok() { + setup_and_sign(); + + uint8_t expected_public_key[] = { + 0x04, 0xa0, 0x24, 0xcb, 0x34, 0xc9, 0x0e, 0xa6, 0xa8, 0xf9, 0xf2, + 0x18, 0x1c, 0x90, 0x20, 0xcb, 0xcc, 0x7c, 0x07, 0x3e, 0x69, 0x98, + 0x17, 0x33, 0xc8, 0xde, 0xed, 0x6f, 0x6c, 0x45, 0x18, 0x22, 0xaa, + 0x08, 0x37, 0x63, 0x50, 0xff, 0x7d, 0xa0, 0x1f, 0x84, 0x2b, 0xb4, + 0x0c, 0x63, 0x1c, 0xbb, 0x71, 0x1f, 0x8b, 0x6f, 0x7a, 0x4f, 0xae, + 0x39, 0x83, 0x20, 0xa3, 0x88, 0x47, 0x74, 0xd2, 0x50, 0xad}; + + uint8_t public_key[sizeof(expected_public_key) + 10]; + uint8_t public_key_length = sizeof(public_key); + + assert(endorsement_get_public_key(public_key, &public_key_length)); + assert(public_key_length == sizeof(expected_public_key)); + assert( + !memcmp(public_key, expected_public_key, sizeof(expected_public_key))); +} + +void test_get_public_key_err_nullbuf() { + setup_and_sign(); + + uint8_t* public_key = NULL; + uint8_t public_key_length = 100; + + assert(!endorsement_get_public_key(public_key, &public_key_length)); + assert(public_key_length == 100); +} + +void test_get_public_key_err_nosignature() { + setup(); + + uint8_t public_key[123]; + uint8_t public_key_length = sizeof(public_key); + + assert(!endorsement_get_public_key(public_key, &public_key_length)); + assert(public_key_length == sizeof(public_key)); +} + +void test_get_public_key_err_buftoosmall() { + setup_and_sign(); + + uint8_t public_key[10]; + uint8_t public_key_length = sizeof(public_key); + + assert(!endorsement_get_public_key(public_key, &public_key_length)); + assert(public_key_length == sizeof(public_key)); +} + +int main() { + printf("Testing endorsement_init()...\n"); + test_endorsement_init_ok(); + test_endorsement_init_err_attinit(); + test_endorsement_init_err_verinit(); + test_endorsement_init_err_selfmt(); + test_endorsement_init_err_getfmt(); + + printf("Testing endorsement_sign()...\n"); + test_signature_ok(); + test_signature_err_notinit(); + test_signature_err_sigbuftoosmall(); + test_signature_err_evibuftoobig(); + test_signature_err_evidencebroken(); + + printf("Testing endorsement_get_envelope()...\n"); + test_get_envelope_ok(); + test_get_envelope_nosignature(); + + printf("Testing endorsement_get_code_hash()...\n"); + test_get_code_hash_ok(); + test_get_code_hash_err_nullbuf(); + test_get_code_hash_err_nosignature(); + test_get_code_hash_err_buftoosmall(); + + printf("Testing endorsement_get_public_key()...\n"); + test_get_public_key_ok(); + test_get_public_key_err_nullbuf(); + test_get_public_key_err_nosignature(); + test_get_public_key_err_buftoosmall(); +} diff --git a/firmware/src/hal/sgx/test/mock/openenclave/attestation/attester.h b/firmware/src/hal/sgx/test/mock/openenclave/attestation/attester.h new file mode 120000 index 00000000..98e8e7ca --- /dev/null +++ b/firmware/src/hal/sgx/test/mock/openenclave/attestation/attester.h @@ -0,0 +1 @@ +../common.h \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/mock/openenclave/attestation/sgx/evidence.h b/firmware/src/hal/sgx/test/mock/openenclave/attestation/sgx/evidence.h new file mode 120000 index 00000000..b84129c3 --- /dev/null +++ b/firmware/src/hal/sgx/test/mock/openenclave/attestation/sgx/evidence.h @@ -0,0 +1 @@ +../../common.h \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/mock/openenclave/attestation/verifier.h b/firmware/src/hal/sgx/test/mock/openenclave/attestation/verifier.h new file mode 120000 index 00000000..98e8e7ca --- /dev/null +++ b/firmware/src/hal/sgx/test/mock/openenclave/attestation/verifier.h @@ -0,0 +1 @@ +../common.h \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/mock/openenclave/bits/attestation.h b/firmware/src/hal/sgx/test/mock/openenclave/bits/attestation.h new file mode 120000 index 00000000..98e8e7ca --- /dev/null +++ b/firmware/src/hal/sgx/test/mock/openenclave/bits/attestation.h @@ -0,0 +1 @@ +../common.h \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/mock/openenclave/bits/defs.h b/firmware/src/hal/sgx/test/mock/openenclave/bits/defs.h new file mode 120000 index 00000000..98e8e7ca --- /dev/null +++ b/firmware/src/hal/sgx/test/mock/openenclave/bits/defs.h @@ -0,0 +1 @@ +../common.h \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/mock/openenclave/bits/sgx/sgxtypes.h b/firmware/src/hal/sgx/test/mock/openenclave/bits/sgx/sgxtypes.h new file mode 120000 index 00000000..b84129c3 --- /dev/null +++ b/firmware/src/hal/sgx/test/mock/openenclave/bits/sgx/sgxtypes.h @@ -0,0 +1 @@ +../../common.h \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/mock/openenclave/common.h b/firmware/src/hal/sgx/test/mock/openenclave/common.h index 54a3fc35..6d04665d 100644 --- a/firmware/src/hal/sgx/test/mock/openenclave/common.h +++ b/firmware/src/hal/sgx/test/mock/openenclave/common.h @@ -25,11 +25,220 @@ #ifndef __MOCK_OE_COMMON_H #define __MOCK_OE_COMMON_H -typedef enum oe_result { +#include +#include + +// Taken from OpenEnclave's include/openenclave/bits/defs.h + +#define OE_PACK_BEGIN _Pragma("pack(push, 1)") +#define OE_PACK_END _Pragma("pack(pop)") + +// Taken from OpenEnclave's include/openenclave/bits/result.h + +typedef enum _oe_result { OE_OK, OE_FAILURE, } oe_result_t; #define oe_result_str(result) ((result) == OE_OK ? "OE_OK" : "OE_FAILURE") +// Taken from OpenEnclave's include/openenclave/bits/sgx/sgxtypes.h + +#define SGX_USERDATA_SIZE 20 +#define OE_ZERO_SIZED_ARRAY +#define OE_SHA256_SIZE 32 +#define SGX_CPUSVN_SIZE 16 +#define OE_INLINE + +OE_PACK_BEGIN +typedef struct _sgx_report_data { + unsigned char field[64]; +} sgx_report_data_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_qe_auth_data { + uint16_t size; + uint8_t* data; +} sgx_qe_auth_data_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_qe_cert_data { + uint16_t type; + uint32_t size; + uint8_t* data; +} sgx_qe_cert_data_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_attributes { + uint64_t flags; + uint64_t xfrm; +} sgx_attributes_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_report_body { + /* (0) CPU security version */ + uint8_t cpusvn[SGX_CPUSVN_SIZE]; + + /* (16) Selector for which fields are defined in SSA.MISC */ + uint32_t miscselect; + + /* (20) Reserved */ + uint8_t reserved1[12]; + + /* (32) Enclave extended product ID */ + uint8_t isvextprodid[16]; + + /* (48) Enclave attributes */ + sgx_attributes_t attributes; + + /* (64) Enclave measurement */ + uint8_t mrenclave[OE_SHA256_SIZE]; + + /* (96) Reserved */ + uint8_t reserved2[32]; + + /* (128) The value of the enclave's SIGNER measurement */ + uint8_t mrsigner[OE_SHA256_SIZE]; + + /* (160) Reserved */ + uint8_t reserved3[32]; + + /* (192) Enclave Configuration ID*/ + uint8_t configid[64]; + + /* (256) Enclave product ID */ + uint16_t isvprodid; + + /* (258) Enclave security version */ + uint16_t isvsvn; + + /* (260) Enclave Configuration Security Version*/ + uint16_t configsvn; + + /* (262) Reserved */ + uint8_t reserved4[42]; + + /* (304) Enclave family ID */ + uint8_t isvfamilyid[16]; + + /* (320) User report data */ + sgx_report_data_t report_data; +} sgx_report_body_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_quote { + /* (0) */ + uint16_t version; + + /* (2) */ + uint16_t sign_type; + + /* (4) */ + uint32_t tee_type; + + /* (8) */ + uint16_t qe_svn; + + /* (10) */ + uint16_t pce_svn; + + /* (12) */ + uint8_t uuid[16]; + + /* (28) */ + uint8_t user_data[SGX_USERDATA_SIZE]; + + /* (48) */ + sgx_report_body_t report_body; + + /* (432) */ + uint32_t signature_len; + + /* (436) signature array (varying length) */ + OE_ZERO_SIZED_ARRAY uint8_t signature[]; +} sgx_quote_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_ecdsa256_signature { + uint8_t r[32]; + uint8_t s[32]; +} sgx_ecdsa256_signature_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_ecdsa256_key { + uint8_t x[32]; + uint8_t y[32]; +} sgx_ecdsa256_key_t; +OE_PACK_END + +OE_PACK_BEGIN +typedef struct _sgx_quote_auth_data { + /* (0) Pair of 256 bit ECDSA Signature. */ + sgx_ecdsa256_signature_t signature; + + /* (64) Pair of 256 bit ECDSA Key. */ + sgx_ecdsa256_key_t attestation_key; + + /* (128) Quoting Enclave Report Body */ + sgx_report_body_t qe_report_body; + + /* (512) Quoting Enclave Report Body Signature */ + sgx_ecdsa256_signature_t qe_report_body_signature; +} sgx_quote_auth_data_t; +OE_PACK_END + +// Taken from OpenEnclave's include/openenclave/bits/evidence.h + +#define OE_FORMAT_UUID_SGX_ECDSA \ + { \ + 0xa3, 0xa2, 0x1e, 0x87, 0x1b, 0x4d, 0x40, 0x14, 0xb7, 0x0a, 0xa1, \ + 0x25, 0xd2, 0xfb, 0xcd, 0x8c \ + } + +#define OE_UUID_SIZE 16 + +typedef struct _oe_uuid_t { + uint8_t b[OE_UUID_SIZE]; +} oe_uuid_t; + +// Taken from OpenEnclave's include/openenclave/attestation/attester.h + +oe_result_t oe_attester_initialize(void); + +oe_result_t oe_attester_select_format(const oe_uuid_t* format_ids, + size_t format_ids_length, + oe_uuid_t* selected_format_id); + +oe_result_t oe_get_evidence(const oe_uuid_t* format_id, + uint32_t flags, + const void* custom_claims_buffer, + size_t custom_claims_buffer_size, + const void* optional_parameters, + size_t optional_parameters_size, + uint8_t** evidence_buffer, + size_t* evidence_buffer_size, + uint8_t** endorsements_buffer, + size_t* endorsements_buffer_size); + +oe_result_t oe_free_evidence(uint8_t* evidence_buffer); + +oe_result_t oe_attester_shutdown(void); + +// Taken from OpenEnclave's include/openenclave/attestation/verifier.h + +oe_result_t oe_verifier_initialize(void); + +oe_result_t oe_verifier_get_format_settings(const oe_uuid_t* format_id, + uint8_t** settings, + size_t* settings_size); + +oe_result_t oe_verifier_shutdown(void); + #endif // #ifndef __MOCK_OE_COMMON_H \ No newline at end of file diff --git a/firmware/src/hal/sgx/test/run-all.sh b/firmware/src/hal/sgx/test/run-all.sh index d059b586..1016ef1b 100755 --- a/firmware/src/hal/sgx/test/run-all.sh +++ b/firmware/src/hal/sgx/test/run-all.sh @@ -2,7 +2,7 @@ if [[ $1 == "exec" ]]; then BASEDIR=$(realpath $(dirname $0)) - TESTDIRS="nvmem secret_store seed" + TESTDIRS="der_utils endorsement nvmem secret_store seed" for d in $TESTDIRS; do echo "******************************" echo "Testing $d..." diff --git a/firmware/src/sgx/src/hsm.edl b/firmware/src/sgx/src/hsm.edl index 9f62a8ba..6411e48a 100644 --- a/firmware/src/sgx/src/hsm.edl +++ b/firmware/src/sgx/src/hsm.edl @@ -9,6 +9,8 @@ enclave { trusted { public bool ecall_system_init(unsigned char *msg_buffer, size_t msg_buffer_size); + public void ecall_system_finalise(); + public unsigned int ecall_system_process_apdu(unsigned int rx); }; diff --git a/firmware/src/sgx/src/trusted/ecall.c b/firmware/src/sgx/src/trusted/ecall.c index cf5c89d4..97f66a76 100644 --- a/firmware/src/sgx/src/trusted/ecall.c +++ b/firmware/src/sgx/src/trusted/ecall.c @@ -32,14 +32,20 @@ #include "hal/log.h" bool ecall_system_init(unsigned char *msg_buffer, size_t msg_buffer_size) { - SYNC_AQUIRE_LOCK(); + SYNC_AQUIRE_LOCK(false); bool success = system_init(msg_buffer, msg_buffer_size); SYNC_RELEASE_LOCK(); return success; } -unsigned int ecall_system_process_apdu(unsigned int rx) { +void ecall_system_finalise() { SYNC_AQUIRE_LOCK(); + system_finalise(); + SYNC_RELEASE_LOCK(); +} + +unsigned int ecall_system_process_apdu(unsigned int rx) { + SYNC_AQUIRE_LOCK(0); unsigned int result = system_process_apdu(rx); SYNC_RELEASE_LOCK(); return result; diff --git a/firmware/src/sgx/src/trusted/ecall.h b/firmware/src/sgx/src/trusted/ecall.h index f5373272..c25910f1 100644 --- a/firmware/src/sgx/src/trusted/ecall.h +++ b/firmware/src/sgx/src/trusted/ecall.h @@ -37,6 +37,11 @@ */ bool ecall_system_init(unsigned char *msg_buffer, size_t msg_buffer_size); +/** + * @brief See system_finalise in system.h + */ +void ecall_system_finalise(); + /** * @brief See system_process_apdu in system.h */ diff --git a/firmware/src/sgx/src/trusted/sync.h b/firmware/src/sgx/src/trusted/sync.h index 853925ff..b079d280 100644 --- a/firmware/src/sgx/src/trusted/sync.h +++ b/firmware/src/sgx/src/trusted/sync.h @@ -28,10 +28,10 @@ #include #include "hal/log.h" -#define SYNC_AQUIRE_LOCK() \ +#define SYNC_AQUIRE_LOCK(err_res) \ if (!sync_try_aqcuire_lock()) { \ LOG("Failed to acquire lock, ecall %s was not executed!\n", __func__); \ - return false; \ + return err_res; \ } #define SYNC_RELEASE_LOCK() sync_release_lock() diff --git a/firmware/src/sgx/src/trusted/system.c b/firmware/src/sgx/src/trusted/system.c index 0f5d4a1b..09fe938c 100644 --- a/firmware/src/sgx/src/trusted/system.c +++ b/firmware/src/sgx/src/trusted/system.c @@ -233,3 +233,8 @@ bool system_init(unsigned char *msg_buffer, size_t msg_buffer_size) { return true; } + +void system_finalise() { + // Finalise modules + endorsement_finalise(); +} diff --git a/firmware/src/sgx/src/trusted/system.h b/firmware/src/sgx/src/trusted/system.h index e571e136..d07a1fd4 100644 --- a/firmware/src/sgx/src/trusted/system.h +++ b/firmware/src/sgx/src/trusted/system.h @@ -35,6 +35,11 @@ */ bool system_init(unsigned char *msg_buffer, size_t msg_buffer_size); +/** + * @brief Finalises the system module + */ +void system_finalise(); + /** * @brief Process an APDU message * diff --git a/firmware/src/sgx/src/untrusted/enclave_proxy.c b/firmware/src/sgx/src/untrusted/enclave_proxy.c index 6e402b56..ee901026 100644 --- a/firmware/src/sgx/src/untrusted/enclave_proxy.c +++ b/firmware/src/sgx/src/untrusted/enclave_proxy.c @@ -37,6 +37,21 @@ bool eprx_system_init(unsigned char *msg_buffer, size_t msg_buffer_size) { return result; } +void eprx_system_finalise() { + oe_enclave_t *enclave = epro_get_enclave(); + if (enclave == NULL) { + LOG("Failed to retrieve the enclave. " + "Unable to call system_finalise().\n"); + return; + } + + oe_result_t oe_result = ecall_system_finalise(enclave); + if (OE_OK != oe_result) { + LOG("Failed to call system_finalise(): oe_result=%u (%s)\n", + oe_result, oe_result_str(oe_result)); + } +} + unsigned int eprx_system_process_apdu(unsigned int rx) { oe_enclave_t *enclave = epro_get_enclave(); if (enclave == NULL) { diff --git a/firmware/src/sgx/src/untrusted/enclave_proxy.h b/firmware/src/sgx/src/untrusted/enclave_proxy.h index 4565ab12..43e59cd5 100644 --- a/firmware/src/sgx/src/untrusted/enclave_proxy.h +++ b/firmware/src/sgx/src/untrusted/enclave_proxy.h @@ -8,6 +8,11 @@ */ bool eprx_system_init(unsigned char *msg_buffer, size_t msg_buffer_size); +/** + * @brief See system_finalise in system.h within the trusted sources + */ +void eprx_system_finalise(); + /** * @brief See system_process_apdu in system.h within the trusted sources */ diff --git a/firmware/src/sgx/src/untrusted/main.c b/firmware/src/sgx/src/untrusted/main.c index 56dfc1fd..9251ef1a 100644 --- a/firmware/src/sgx/src/untrusted/main.c +++ b/firmware/src/sgx/src/untrusted/main.c @@ -96,8 +96,8 @@ static struct argp argp = { static void finalise_with(int exit_code) { printf("Terminating...\n"); + eprx_system_finalise(); io_finalise(); - // TODO: finalize enclave, i/o printf("Done. Bye.\n"); exit(exit_code); } diff --git a/firmware/src/sgx/test/system/test_system.c b/firmware/src/sgx/test/system/test_system.c index b0c0b760..d49fdf3d 100644 --- a/firmware/src/sgx/test/system/test_system.c +++ b/firmware/src/sgx/test/system/test_system.c @@ -276,6 +276,10 @@ bool endorsement_init() { return true; } +void endorsement_finalise() { + // Nothing to do here +} + void nvmem_init() { NUM_CALLS(nvmem_init)++; } From 0663ff7c132200fddf7065270d1597daf50a97dd Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Sat, 7 Dec 2024 02:42:04 +1300 Subject: [PATCH 08/21] SGX attestation gathering (#228) - Unified certificates in certificate module - Moved existing HSMCertificate and HSMCertificateElement to certificate_v1 - Added new HSMCertificateV2 and HSMCertificateV2Element (and subclasses) - Added new attestation gathering operation for adm_sgx - Added new attestation gathering module used within adm_sgx - Factored out user-defined value gathering into admin/misc module - Current SGX attestation saving to file is directly translated from value grabbed with HSM2Dongle - Added and updated unit tests --- middleware/adm_ledger.py | 4 +- middleware/adm_sgx.py | 15 +- middleware/admin/certificate.py | 230 +--------------- middleware/admin/certificate_v1.py | 251 ++++++++++++++++++ middleware/admin/certificate_v2.py | 49 ++++ middleware/admin/ledger_attestation.py | 28 +- middleware/admin/misc.py | 28 ++ middleware/admin/sgx_attestation.py | 71 +++++ middleware/tests/admin/test_adm_sgx.py | 40 ++- ..._certificate.py => test_certificate_v1.py} | 4 +- ...ment.py => test_certificate_v1_element.py} | 2 +- .../tests/admin/test_ledger_attestation.py | 81 +----- middleware/tests/admin/test_misc.py | 63 +++++ .../tests/admin/test_sgx_attestation.py | 113 ++++++++ setup.cfg | 4 +- 15 files changed, 654 insertions(+), 329 deletions(-) create mode 100644 middleware/admin/certificate_v1.py create mode 100644 middleware/admin/certificate_v2.py create mode 100644 middleware/admin/sgx_attestation.py rename middleware/tests/admin/{test_certificate.py => test_certificate_v1.py} (99%) rename middleware/tests/admin/{test_certificate_element.py => test_certificate_v1_element.py} (99%) create mode 100644 middleware/tests/admin/test_misc.py create mode 100644 middleware/tests/admin/test_sgx_attestation.py diff --git a/middleware/adm_ledger.py b/middleware/adm_ledger.py index 758060e6..6ba72dce 100644 --- a/middleware/adm_ledger.py +++ b/middleware/adm_ledger.py @@ -25,7 +25,7 @@ import logging from ledger.hsm2dongle import HSM2DongleError from comm.platform import Platform -from admin.misc import not_implemented, info, AdminError +from admin.misc import not_implemented, info, AdminError, DEFAULT_ATT_UD_SOURCE from admin.unlock import do_unlock from admin.onboard import do_onboard from admin.pubkeys import do_get_pubkeys @@ -34,8 +34,6 @@ from admin.verify_ledger_attestation import do_verify_attestation from admin.authorize_signer import do_authorize_signer -DEFAULT_ATT_UD_SOURCE = "https://public-node.rsk.co" - def main(): logging.disable(logging.CRITICAL) diff --git a/middleware/adm_sgx.py b/middleware/adm_sgx.py index 6a4dcf0c..d6a8f289 100644 --- a/middleware/adm_sgx.py +++ b/middleware/adm_sgx.py @@ -25,11 +25,12 @@ import logging from ledger.hsm2dongle import HSM2DongleError from comm.platform import Platform -from admin.misc import not_implemented, info, AdminError +from admin.misc import not_implemented, info, AdminError, DEFAULT_ATT_UD_SOURCE 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 +from admin.sgx_attestation import do_attestation def main(): @@ -40,6 +41,7 @@ def main(): "onboard": do_onboard, "pubkeys": do_get_pubkeys, "changepin": do_changepin, + "attestation": do_attestation, } parser = ArgumentParser(description="SGX powHSM Administrative tool") @@ -79,7 +81,7 @@ def main(): "-o", "--output", dest="output_file_path", - help="Output file (only valid for 'onboard' and 'pubkeys' " + help="Output file (only valid for 'onboard', 'pubkeys' and 'attestation' " "operations).", ) parser.add_argument( @@ -92,6 +94,15 @@ def main(): default=False, const=True, ) + parser.add_argument( + "--attudsource", + dest="attestation_ud_source", + default=DEFAULT_ATT_UD_SOURCE, + help="JSON-RPC endpoint used to retrieve the latest RSK block hash used " + "as the user defined value for the attestation (defaults to " + f"{DEFAULT_ATT_UD_SOURCE}). Can also specify a 32-byte hex string to use as" + " the value.", + ) parser.add_argument( "-v", "--verbose", diff --git a/middleware/admin/certificate.py b/middleware/admin/certificate.py index 29ce300a..64b3bb6b 100644 --- a/middleware/admin/certificate.py +++ b/middleware/admin/certificate.py @@ -20,231 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import json -import hmac -import secp256k1 as ec -import hashlib -from .utils import is_nonempty_hex_string - - -class HSMCertificate: - VERSION = 1 # Only supported version - ROOT_ELEMENT = "root" - - @staticmethod - def from_jsonfile(path): - try: - with open(path, "r") as file: - certificate_map = json.loads(file.read()) - - if type(certificate_map) != dict: - raise ValueError( - "JSON file must contain an object as a top level element") - - return HSMCertificate(certificate_map) - except (ValueError, json.JSONDecodeError) as e: - raise ValueError('Unable to read HSM certificate from "%s": %s' % - (path, str(e))) - - def __init__(self, certificate_map=None): - self._targets = [] - self._elements = {} - - if certificate_map is not None: - self._parse(certificate_map) - - def validate_and_get_values(self, raw_root_pubkey_hex): - # Parse the root public key - try: - root_pubkey = ec.PublicKey(bytes.fromhex(raw_root_pubkey_hex), raw=True) - except Exception: - return dict([(target, (False, self.ROOT_ELEMENT)) - for target in self._targets]) - - result = {} - for target in self._targets: - # Build the chain from the target to the root - chain = [] - current = self._elements[target] - while True: - if current.signed_by == self.ROOT_ELEMENT: - break - chain.append(current) - current = self._elements[current.signed_by] - - # Validate the chain from root to leaf - # If valid, return True and the value of the leaf - # If not valid, return False and the name of the element that - # failed the validation - current_pubkey = root_pubkey - while True: - # Validate this element - if not current.is_valid(current_pubkey): - result[target] = (False, current.name) - break - # Reached the leaf? => valid! - if len(chain) == 0: - result[target] = (True, current.get_value(), current.tweak) - break - - current_pubkey = ec.PublicKey(bytes.fromhex(current.get_value()), - raw=True) - current = chain.pop() - - return result - - def add_element(self, element): - if type(element) != HSMCertificateElement: - raise ValueError( - f"Expected an HSMCertificateElement but got a {type(element)}") - self._elements[element.name] = element - - def clear_targets(self): - self._targets = [] - - def add_target(self, target): - if target not in self._elements: - raise ValueError(f"Target {target} not in elements") - self._targets.append(target) - - def to_dict(self): - return { - "version": self.VERSION, - "targets": self._targets, - "elements": list(map(lambda e: e.to_dict(), self._elements.values())), - } - - def save_to_jsonfile(self, path): - with open(path, "w") as file: - file.write("%s\n" % json.dumps(self.to_dict(), indent=2)) - - def _parse(self, certificate_map): - if "version" not in certificate_map or certificate_map["version"] != self.VERSION: - raise ValueError( - "Invalid or unsupported HSM certificate version " - f"(current version is {self.VERSION})" - ) - - if "targets" not in certificate_map or type(certificate_map["targets"]) != list: - raise ValueError("Missing or invalid targets") - - self._targets = certificate_map["targets"] - - if "elements" not in certificate_map: - raise ValueError("Missing elements") - - for item in certificate_map["elements"]: - element = HSMCertificateElement(item) - self._elements[item["name"]] = element - - # Sanity: check each target has a path to the root authority - for target in self._targets: - if target not in self._elements: - raise ValueError(f"Target {target} not in elements") - - visited = [] - current = self._elements[target] - while True: - if current.name in visited: - raise ValueError( - f"Target {target} has not got a path to the root authority") - if current.signed_by == self.ROOT_ELEMENT: - break - if current.signed_by not in self._elements: - raise ValueError(f"Signer {current.signed_by} not in elements") - visited.append(current.name) - current = self._elements[current.signed_by] - - -class HSMCertificateElement: - VALID_NAMES = ["device", "attestation", "ui", "signer"] - EXTRACTORS = { - "device": lambda b: b[-65:], - "attestation": lambda b: b[1:], - "ui": lambda b: b[:], - "signer": lambda b: b[:], - } - - def __init__(self, element_map): - if ("name" not in element_map - or element_map["name"] not in self.VALID_NAMES): - raise ValueError("Missing or invalid name for HSM certificate element") - self._name = element_map["name"] - - if "signed_by" not in element_map: - raise ValueError("Missing certifier for HSM certificate element") - self._signed_by = element_map["signed_by"] - - self._tweak = None - if "tweak" in element_map: - if not is_nonempty_hex_string(element_map["tweak"]): - raise ValueError( - f"Invalid signer tweak for HSM certificate element {self.name}") - self._tweak = element_map["tweak"] - - if "message" not in element_map or not is_nonempty_hex_string( - element_map["message"]): - raise ValueError( - f"Missing or invalid message for HSM certificate element {self.name}") - self._message = element_map["message"] - - if "signature" not in element_map or not is_nonempty_hex_string( - element_map["signature"]): - raise ValueError( - f"Missing or invalid signature for HSM certificate element {self.name}") - self._signature = element_map["signature"] - - @property - def name(self): - return self._name - - @property - def signed_by(self): - return self._signed_by - - @property - def tweak(self): - return self._tweak - - @property - def message(self): - return self._message - - @property - def signature(self): - return self._signature - - def to_dict(self): - result = { - "name": self.name, - "message": self.message, - "signature": self.signature, - "signed_by": self.signed_by, - } - - if self.tweak is not None: - result["tweak"] = self.tweak - - return result - - def is_valid(self, certifier_pubkey): - try: - message = bytes.fromhex(self.message) - - verifier_pubkey = certifier_pubkey - if self.tweak is not None: - tweak = hmac.new( - bytes.fromhex(self.tweak), - certifier_pubkey.serialize(compressed=False), - hashlib.sha256, - ).digest() - - verifier_pubkey = verifier_pubkey.tweak_add(tweak) - - return verifier_pubkey.ecdsa_verify( - message, verifier_pubkey.ecdsa_deserialize(bytes.fromhex(self.signature))) - except Exception: - return False - - def get_value(self): - return self.EXTRACTORS[self.name](bytes.fromhex(self.message)).hex() +from .certificate_v1 import HSMCertificate, HSMCertificateElement +from .certificate_v2 import HSMCertificateV2, HSMCertificateV2Element diff --git a/middleware/admin/certificate_v1.py b/middleware/admin/certificate_v1.py new file mode 100644 index 00000000..a37802ed --- /dev/null +++ b/middleware/admin/certificate_v1.py @@ -0,0 +1,251 @@ +# 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 json +import hmac +import secp256k1 as ec +import hashlib +from .utils import is_nonempty_hex_string + + +class HSMCertificateElement: + VALID_NAMES = ["device", "attestation", "ui", "signer"] + EXTRACTORS = { + "device": lambda b: b[-65:], + "attestation": lambda b: b[1:], + "ui": lambda b: b[:], + "signer": lambda b: b[:], + } + + def __init__(self, element_map): + if ("name" not in element_map + or element_map["name"] not in self.VALID_NAMES): + raise ValueError("Missing or invalid name for HSM certificate element") + self._name = element_map["name"] + + if "signed_by" not in element_map: + raise ValueError("Missing certifier for HSM certificate element") + self._signed_by = element_map["signed_by"] + + self._tweak = None + if "tweak" in element_map: + if not is_nonempty_hex_string(element_map["tweak"]): + raise ValueError( + f"Invalid signer tweak for HSM certificate element {self.name}") + self._tweak = element_map["tweak"] + + if "message" not in element_map or not is_nonempty_hex_string( + element_map["message"]): + raise ValueError( + f"Missing or invalid message for HSM certificate element {self.name}") + self._message = element_map["message"] + + if "signature" not in element_map or not is_nonempty_hex_string( + element_map["signature"]): + raise ValueError( + f"Missing or invalid signature for HSM certificate element {self.name}") + self._signature = element_map["signature"] + + @property + def name(self): + return self._name + + @property + def signed_by(self): + return self._signed_by + + @property + def tweak(self): + return self._tweak + + @property + def message(self): + return self._message + + @property + def signature(self): + return self._signature + + def to_dict(self): + result = { + "name": self.name, + "message": self.message, + "signature": self.signature, + "signed_by": self.signed_by, + } + + if self.tweak is not None: + result["tweak"] = self.tweak + + return result + + def is_valid(self, certifier_pubkey): + try: + message = bytes.fromhex(self.message) + + verifier_pubkey = certifier_pubkey + if self.tweak is not None: + tweak = hmac.new( + bytes.fromhex(self.tweak), + certifier_pubkey.serialize(compressed=False), + hashlib.sha256, + ).digest() + + verifier_pubkey = verifier_pubkey.tweak_add(tweak) + + return verifier_pubkey.ecdsa_verify( + message, verifier_pubkey.ecdsa_deserialize(bytes.fromhex(self.signature))) + except Exception: + return False + + def get_value(self): + return self.EXTRACTORS[self.name](bytes.fromhex(self.message)).hex() + + +class HSMCertificate: + VERSION = 1 # Only supported version + ROOT_ELEMENT = "root" + ELEMENT_BASE_CLASS = HSMCertificateElement + + @staticmethod + def from_jsonfile(path): + try: + with open(path, "r") as file: + certificate_map = json.loads(file.read()) + + if type(certificate_map) != dict: + raise ValueError( + "JSON file must contain an object as a top level element") + + return HSMCertificate(certificate_map) + except (ValueError, json.JSONDecodeError) as e: + raise ValueError('Unable to read HSM certificate from "%s": %s' % + (path, str(e))) + + def __init__(self, certificate_map=None): + self._targets = [] + self._elements = {} + + if certificate_map is not None: + self._parse(certificate_map) + + def validate_and_get_values(self, raw_root_pubkey_hex): + # Parse the root public key + try: + root_pubkey = ec.PublicKey(bytes.fromhex(raw_root_pubkey_hex), raw=True) + except Exception: + return dict([(target, (False, self.ROOT_ELEMENT)) + for target in self._targets]) + + result = {} + for target in self._targets: + # Build the chain from the target to the root + chain = [] + current = self._elements[target] + while True: + if current.signed_by == self.ROOT_ELEMENT: + break + chain.append(current) + current = self._elements[current.signed_by] + + # Validate the chain from root to leaf + # If valid, return True and the value of the leaf + # If not valid, return False and the name of the element that + # failed the validation + current_pubkey = root_pubkey + while True: + # Validate this element + if not current.is_valid(current_pubkey): + result[target] = (False, current.name) + break + # Reached the leaf? => valid! + if len(chain) == 0: + result[target] = (True, current.get_value(), current.tweak) + break + + current_pubkey = ec.PublicKey(bytes.fromhex(current.get_value()), + raw=True) + current = chain.pop() + + return result + + def add_element(self, element): + if not isinstance(element, self.ELEMENT_BASE_CLASS): + raise ValueError( + f"Expected an HSMCertificateElement but got a {type(element)}") + self._elements[element.name] = element + + def clear_targets(self): + self._targets = [] + + def add_target(self, target): + if target not in self._elements: + raise ValueError(f"Target {target} not in elements") + self._targets.append(target) + + def to_dict(self): + return { + "version": self.VERSION, + "targets": self._targets, + "elements": list(map(lambda e: e.to_dict(), self._elements.values())), + } + + def save_to_jsonfile(self, path): + with open(path, "w") as file: + file.write("%s\n" % json.dumps(self.to_dict(), indent=2)) + + def _parse(self, certificate_map): + if "version" not in certificate_map or certificate_map["version"] != self.VERSION: + raise ValueError( + "Invalid or unsupported HSM certificate version " + f"(current version is {self.VERSION})" + ) + + if "targets" not in certificate_map or type(certificate_map["targets"]) != list: + raise ValueError("Missing or invalid targets") + + self._targets = certificate_map["targets"] + + if "elements" not in certificate_map: + raise ValueError("Missing elements") + + for item in certificate_map["elements"]: + element = HSMCertificateElement(item) + self._elements[item["name"]] = element + + # Sanity: check each target has a path to the root authority + for target in self._targets: + if target not in self._elements: + raise ValueError(f"Target {target} not in elements") + + visited = [] + current = self._elements[target] + while True: + if current.name in visited: + raise ValueError( + f"Target {target} has not got a path to the root authority") + if current.signed_by == self.ROOT_ELEMENT: + break + if current.signed_by not in self._elements: + raise ValueError(f"Signer {current.signed_by} not in elements") + visited.append(current.name) + current = self._elements[current.signed_by] diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py new file mode 100644 index 00000000..61e74ce5 --- /dev/null +++ b/middleware/admin/certificate_v2.py @@ -0,0 +1,49 @@ +# 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. + +from .certificate_v1 import HSMCertificate + + +class HSMCertificateV2Element: + # TODO: actual logic and subclasses + def __init__(self, element_map): + self.element_map = element_map + + # Stub + def name(self): + return "attestation" + + def to_dict(self): + return self.element_map + + +class HSMCertificateV2(HSMCertificate): + VERSION = 2 + ELEMENT_BASE_CLASS = HSMCertificateV2Element + + def validate_and_get_values(self, raw_root_pubkey_hex): + # TODO + pass + + def _parse(self, certificate_map): + # TODO + pass diff --git a/middleware/admin/ledger_attestation.py b/middleware/admin/ledger_attestation.py index a10fa2e0..9242d08e 100644 --- a/middleware/admin/ledger_attestation.py +++ b/middleware/admin/ledger_attestation.py @@ -20,13 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from .misc import info, head, get_hsm, dispose_hsm, AdminError, wait_for_reconnection -from .utils import is_hex_string_of_length, normalize_hex_string +from .misc import info, head, get_hsm, dispose_hsm, AdminError, wait_for_reconnection, \ + get_ud_value_for_attestation from .unlock import do_unlock from .certificate import HSMCertificate, HSMCertificateElement -from .rsk_client import RskClient, RskClientError - -UD_VALUE_LENGTH = 32 def do_attestation(options): @@ -46,24 +43,9 @@ def do_attestation(options): except Exception as e: raise AdminError(f"While loading the attestation certificate file: {str(e)}") - # Get the UD value for the UI attestation - info("Gathering user-defined UI attestation value... ", options.verbose) - - if is_hex_string_of_length(options.attestation_ud_source, - UD_VALUE_LENGTH, - allow_prefix=True): - ud_value = normalize_hex_string(options.attestation_ud_source) - else: - try: - rsk_client = RskClient(options.attestation_ud_source) - best_block = rsk_client.get_block_by_number( - rsk_client.get_best_block_number()) - ud_value = best_block["hash"][2:] - if not is_hex_string_of_length(ud_value, UD_VALUE_LENGTH): - raise ValueError(f"Got invalid best block from RSK server: {ud_value}") - except RskClientError as e: - raise AdminError(f"While fetching the best RSK block hash: {str(e)}") - + # Get the UD value for the attestations + info("Gathering user-defined attestation value... ", options.verbose) + ud_value = get_ud_value_for_attestation(options.attestation_ud_source) info(f"Using {ud_value} as the user-defined attestation value") # Attempt to unlock the device without exiting the UI diff --git a/middleware/admin/misc.py b/middleware/admin/misc.py index c8dcd59b..e72e46f7 100644 --- a/middleware/admin/misc.py +++ b/middleware/admin/misc.py @@ -29,6 +29,9 @@ from .dongle_admin import DongleAdmin from .dongle_eth import DongleEth from comm.platform import Platform +from .utils import is_hex_string_of_length, normalize_hex_string +from .rsk_client import RskClient, RskClientError + PIN_ERROR_MESSAGE = ("Invalid pin given. It must be exactly 8 alphanumeric " "characters with at least one alphabetic character.") @@ -37,6 +40,9 @@ SIGNER_WAIT_TIME = 3 # seconds +ATTESTATION_UD_VALUE_LENGTH = 32 # bytes +DEFAULT_ATT_UD_SOURCE = "https://public-node.rsk.co" + class AdminError(RuntimeError): pass @@ -127,3 +133,25 @@ def ask_for_pin(any_pin): def wait_for_reconnection(): time.sleep(SIGNER_WAIT_TIME) + + +def get_ud_value_for_attestation(user_provided_ud_source): + if is_hex_string_of_length(user_provided_ud_source, + ATTESTATION_UD_VALUE_LENGTH, + allow_prefix=True): + # Final value provided by user + ud_value = normalize_hex_string(user_provided_ud_source) + else: + # Final value taken from a specific Rootstock node + try: + rsk_client = RskClient(user_provided_ud_source) + best_block = rsk_client.get_block_by_number( + rsk_client.get_best_block_number()) + ud_value = best_block["hash"][2:] + if not is_hex_string_of_length(ud_value, ATTESTATION_UD_VALUE_LENGTH): + raise ValueError("Got invalid best block from " + f"Rootstock server: {ud_value}") + except RskClientError as e: + raise AdminError(f"While fetching the best Rootstock block hash: {str(e)}") + + return ud_value diff --git a/middleware/admin/sgx_attestation.py b/middleware/admin/sgx_attestation.py new file mode 100644 index 00000000..c261f0ba --- /dev/null +++ b/middleware/admin/sgx_attestation.py @@ -0,0 +1,71 @@ +# 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. + +from .misc import info, head, get_hsm, AdminError, get_ud_value_for_attestation +from .unlock import do_unlock +from .certificate import HSMCertificateV2, HSMCertificateV2Element + + +def do_attestation(options): + head("### -> Get powHSM attestation", fill="#") + hsm = None + + # Require an output file + if options.output_file_path is None: + raise AdminError("No output file path given") + + # Get the UD value for the attestation + info("Gathering user-defined attestation value... ", options.verbose) + ud_value = get_ud_value_for_attestation(options.attestation_ud_source) + info(f"Using {ud_value} as the user-defined attestation value") + + # Attempt to unlock the device + if not options.no_unlock: + try: + do_unlock(options, label=False) + except Exception as e: + raise AdminError(f"Failed to unlock device: {str(e)}") + + # Connection + hsm = get_hsm(options.verbose) + + # powHSM attestation + info("Gathering powHSM attestation... ", options.verbose) + try: + powhsm_attestation = hsm.get_powhsm_attestation(ud_value) + except Exception as e: + raise AdminError(f"Failed to gather powHSM attestation: {str(e)}") + info("powHSM attestation gathered") + + hsm.disconnect() + + # Generate and save the attestation certificate + info("Generating the attestation certificate... ", options.verbose) + + att_cert = HSMCertificateV2() + # TODO: + # 1. Parse envelope + # 2. Add actual elements of the certificate + att_cert.add_element(HSMCertificateV2Element(powhsm_attestation)) + att_cert.save_to_jsonfile(options.output_file_path) + + info(f"Attestation certificate saved to {options.output_file_path}") diff --git a/middleware/tests/admin/test_adm_sgx.py b/middleware/tests/admin/test_adm_sgx.py index d5a5f5cb..53c139cd 100644 --- a/middleware/tests/admin/test_adm_sgx.py +++ b/middleware/tests/admin/test_adm_sgx.py @@ -30,7 +30,7 @@ logging.disable(logging.CRITICAL) -class TestAdmLedger(TestCase): +class TestAdmSgx(TestCase): def setUp(self): self.old_stdout = sys.stdout self.DEFAULT_OPTIONS = { @@ -39,6 +39,7 @@ def setUp(self): "any_pin": False, "new_pin": None, "no_unlock": False, + "attestation_ud_source": "https://public-node.rsk.co", "operation": None, "output_file_path": None, "pin": None, @@ -169,3 +170,40 @@ def test_changepin(self, do_changepin): self.assertTrue(do_changepin.called) self.assertEqual(do_changepin.call_count, 2) self.assertEqual(expected_call_args_list, do_changepin.call_args_list) + + @patch("adm_sgx.do_attestation") + def test_attestation(self, do_attestation): + expected_options = { + **self.DEFAULT_OPTIONS, + 'attestation_ud_source': 'user-defined-source', + 'operation': 'attestation', + 'output_file_path': 'out-path', + 'pin': 'a-pin', + } + expected_call_args_list = [ + call(Namespace(**expected_options)), + call(Namespace(**{**expected_options, "no_unlock": True})) + ] + + with patch('sys.argv', ['adm_sgx.py', + '-p', 'a-pin', + '-o', 'out-path', + '--attudsource', 'user-defined-source', + 'attestation']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + with patch('sys.argv', ['adm_sgx.py', + '--pin', 'a-pin', + '--output', 'out-path', + '--attudsource', 'user-defined-source', + '--nounlock', + 'attestation']): + with self.assertRaises(SystemExit) as e: + main() + self.assertEqual(e.exception.code, 0) + + self.assertTrue(do_attestation.called) + self.assertEqual(do_attestation.call_count, 2) + self.assertEqual(expected_call_args_list, do_attestation.call_args_list) diff --git a/middleware/tests/admin/test_certificate.py b/middleware/tests/admin/test_certificate_v1.py similarity index 99% rename from middleware/tests/admin/test_certificate.py rename to middleware/tests/admin/test_certificate_v1.py index 856e414e..591cfc91 100644 --- a/middleware/tests/admin/test_certificate.py +++ b/middleware/tests/admin/test_certificate_v1.py @@ -29,7 +29,7 @@ from admin.certificate import HSMCertificate, HSMCertificateElement -class TestCertificate(TestCase): +class TestHSMCertificate(TestCase): def test_create_valid_certificate_ok(self): cert = HSMCertificate({ "version": 1, @@ -155,7 +155,7 @@ def test_create_certificate_missing_elements(self): "targets": ["attestation", "device"] }) - @patch('admin.certificate.HSMCertificateElement') + @patch('admin.certificate_v1.HSMCertificateElement') def test_create_certificate_invalid_element(self, certElementMock): certElementMock.side_effect = ValueError() with self.assertRaises(ValueError): diff --git a/middleware/tests/admin/test_certificate_element.py b/middleware/tests/admin/test_certificate_v1_element.py similarity index 99% rename from middleware/tests/admin/test_certificate_element.py rename to middleware/tests/admin/test_certificate_v1_element.py index 74087f1d..ff2f206c 100644 --- a/middleware/tests/admin/test_certificate_element.py +++ b/middleware/tests/admin/test_certificate_v1_element.py @@ -30,7 +30,7 @@ from admin.certificate import HSMCertificateElement -class TestCertificateElement(TestCase): +class TestHSMCertificateElement(TestCase): def test_create_certificate_element_ok(self): element = HSMCertificateElement({ "name": "device", diff --git a/middleware/tests/admin/test_ledger_attestation.py b/middleware/tests/admin/test_ledger_attestation.py index 935b1cc2..27ab2672 100644 --- a/middleware/tests/admin/test_ledger_attestation.py +++ b/middleware/tests/admin/test_ledger_attestation.py @@ -28,7 +28,6 @@ from admin.ledger_attestation import do_attestation from admin.certificate import HSMCertificate from admin.misc import AdminError -from admin.rsk_client import RskClientError @patch("sys.stdout.write") @@ -79,68 +78,11 @@ def setupDefaultOptions(self): options.attestation_ud_source = 'aa' * 32 return options - @patch('admin.ledger_attestation.RskClient') - def test_attestation_ok_provided_ud_value(self, - RskClient, - from_jsonfile, - get_hsm, - *_): + @patch('admin.ledger_attestation.get_ud_value_for_attestation') + def test_attestation_ok(self, get_ud_value_for_attestation, + from_jsonfile, get_hsm, *_): self.setupMocks(from_jsonfile, get_hsm) - options = self.setupDefaultOptions() - with patch('builtins.open', mock_open()) as file_mock: - do_attestation(options) - - self.assertEqual([call(options.attestation_ud_source)], - get_hsm.return_value.get_ui_attestation.call_args_list) - self.assertEqual([], RskClient.call_args_list) - self.assertEqual([call(options.attestation_certificate_file_path)], - from_jsonfile.call_args_list) - self.assertEqual([call(options.verbose), call(options.verbose)], - get_hsm.call_args_list) - self.assertEqual([call(options.output_file_path, 'w')], file_mock.call_args_list) - self.assertEqual([call("%s\n" % json.dumps({ - 'version': 1, - 'targets': [ - 'ui', - 'signer' - ], - 'elements': [ - { - "name": "attestation", - "message": '11' * 32, - "signature": '22' * 32, - "signed_by": "device" - }, - { - "name": "device", - "message": '33' * 32, - "signature": '44' * 32, - "signed_by": "root" - }, - { - 'name': 'ui', - 'message': 'aa' * 32, - 'signature': 'bb' * 32, - 'signed_by': 'attestation', - 'tweak': 'cc' * 32 - }, - { - 'name': 'signer', - 'message': 'dd' * 32, - 'signature': 'ee' * 32, - 'signed_by': 'attestation', - 'tweak': 'ff' * 32 - } - ] - }, indent=2))], - file_mock.return_value.write.call_args_list) - - @patch('admin.ledger_attestation.RskClient') - def test_attestation_ok_get_ud_value(self, RskClient, from_jsonfile, get_hsm, *_): - self.setupMocks(from_jsonfile, get_hsm) - RskClient.return_value = Mock() - rsk_client = RskClient.return_value - rsk_client.get_block_by_number = Mock(return_value={'hash': '0x' + 'bb' * 32}) + get_ud_value_for_attestation.return_value = 'bb'*32 options = self.setupDefaultOptions() options.attestation_ud_source = 'an-url' @@ -149,8 +91,7 @@ def test_attestation_ok_get_ud_value(self, RskClient, from_jsonfile, get_hsm, *_ self.assertEqual([call(options.attestation_certificate_file_path)], from_jsonfile.call_args_list) - self.assertEqual([call('an-url')], RskClient.call_args_list) - self.assertTrue(rsk_client.get_block_by_number.called) + self.assertEqual([call('an-url')], get_ud_value_for_attestation.call_args_list) self.assertNotEqual([call(options.attestation_ud_source)], get_hsm.return_value.get_ui_attestation.call_args_list) self.assertEqual([call('bb' * 32)], @@ -228,15 +169,19 @@ def test_attestation_invalid_jsonfile(self, from_jsonfile, get_hsm, *_): self.assertFalse(get_hsm.called) self.assertFalse(file_mock.return_value.write.called) - @patch('admin.ledger_attestation.RskClient') - def test_attestation_rsk_client_error(self, RskClient, from_jsonfile, get_hsm, *_): + @patch('admin.ledger_attestation.get_ud_value_for_attestation') + def test_attestation_get_ud_value_for_attestation_error(self, + get_ud_value_for_attestation, + from_jsonfile, get_hsm, *_): self.setupMocks(from_jsonfile, get_hsm) - RskClient.side_effect = RskClientError('error-msg') + get_ud_value_for_attestation.side_effect = AdminError('error-msg') options = self.setupDefaultOptions() - options.attestation_ud_source = 'an-url' + options.attestation_ud_source = 'another-url' with patch('builtins.open', mock_open()) as file_mock: with self.assertRaises(AdminError): do_attestation(options) + self.assertEqual([call('another-url')], + get_ud_value_for_attestation.call_args_list) self.assertTrue(from_jsonfile.called) self.assertFalse(get_hsm.called) self.assertFalse(file_mock.return_value.write.called) diff --git a/middleware/tests/admin/test_misc.py b/middleware/tests/admin/test_misc.py new file mode 100644 index 00000000..d6ac6279 --- /dev/null +++ b/middleware/tests/admin/test_misc.py @@ -0,0 +1,63 @@ +# 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. + +from unittest import TestCase +from unittest.mock import Mock, patch +from admin.misc import get_ud_value_for_attestation, RskClientError, AdminError + +import logging + +logging.disable(logging.CRITICAL) + + +class TestGetUdValueForAttestation(TestCase): + def test_hex_string(self): + self.assertEqual("aa"*32, get_ud_value_for_attestation("aa"*32)) + self.assertEqual("aa"*32, get_ud_value_for_attestation("0x"+"aa"*32)) + + @patch("admin.misc.RskClient") + def test_ud_source_ok(self, RskClient): + rsk_client = Mock() + RskClient.return_value = rsk_client + rsk_client.get_best_block_number.return_value = "the-best-block-number" + rsk_client.get_block_by_number.return_value = {"hash": "0x" + "bb"*32} + + self.assertEqual("bb"*32, get_ud_value_for_attestation("a-ud-source")) + + RskClient.assert_called_with("a-ud-source") + rsk_client.get_best_block_number.assert_called() + rsk_client.get_block_by_number.assert_called_with("the-best-block-number") + + @patch("admin.misc.RskClient") + def test_ud_source_client_error(self, RskClient): + rsk_client = Mock() + RskClient.return_value = rsk_client + rsk_client.get_best_block_number.return_value = "the-best-block-number" + rsk_client.get_block_by_number.side_effect = RskClientError("an-error") + + with self.assertRaises(AdminError) as e: + get_ud_value_for_attestation("a-ud-source") + self.assertIn("an-error", str(e.exception)) + + RskClient.assert_called_with("a-ud-source") + rsk_client.get_best_block_number.assert_called() + rsk_client.get_block_by_number.assert_called_with("the-best-block-number") diff --git a/middleware/tests/admin/test_sgx_attestation.py b/middleware/tests/admin/test_sgx_attestation.py new file mode 100644 index 00000000..2f05f2da --- /dev/null +++ b/middleware/tests/admin/test_sgx_attestation.py @@ -0,0 +1,113 @@ +# 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. + +from types import SimpleNamespace +from unittest import TestCase +from unittest.mock import Mock, patch +from parameterized import parameterized +from admin.sgx_attestation import do_attestation +from admin.misc import AdminError + + +@patch("sys.stdout") +@patch("admin.sgx_attestation.HSMCertificateV2Element") +@patch("admin.sgx_attestation.HSMCertificateV2") +@patch("admin.sgx_attestation.do_unlock") +@patch("admin.sgx_attestation.get_ud_value_for_attestation") +@patch("admin.sgx_attestation.get_hsm") +class TestSgxAttestation(TestCase): + def setUp(self): + options = SimpleNamespace() + options.output_file_path = "an-output-file" + options.no_unlock = False + options.verbose = "is-verbose" + options.attestation_ud_source = "an-ud-source" + self.options = options + + def setupMocks(self, get_hsm, get_ud_value_for_attestation, do_unlock, + HSMCertificateV2, HSMCertificateV2Element): + self.get_hsm = get_hsm + self.get_ud_value_for_attestation = get_ud_value_for_attestation + self.do_unlock = do_unlock + self.HSMCertificateV2 = HSMCertificateV2 + self.HSMCertificateV2Element = HSMCertificateV2Element + + self.hsm = Mock() + self.hsm.get_powhsm_attestation.return_value = "the-attestation" + get_hsm.return_value = self.hsm + get_ud_value_for_attestation.return_value = "some-random-value" + + @parameterized.expand([ + ("unlock", False), + ("no_unlock", True), + ]) + def test_ok(self, *args): + self.setupMocks(*args[:-3]) + self.options.no_unlock = args[-1] + + do_attestation(self.options) + + self.get_ud_value_for_attestation.assert_called_with("an-ud-source") + if self.options.no_unlock: + self.do_unlock.assert_not_called() + else: + self.do_unlock.assert_called_with(self.options, label=False) + self.get_hsm.assert_called_with("is-verbose") + self.hsm.get_powhsm_attestation.assert_called_with("some-random-value") + self.hsm.disconnect.assert_called() + self.HSMCertificateV2Element.assert_called_with("the-attestation") + elem = self.HSMCertificateV2Element.return_value + cert = self.HSMCertificateV2.return_value + cert.add_element.assert_called_with(elem) + cert.save_to_jsonfile.assert_called_with("an-output-file") + + def test_no_output_path(self, *args): + self.setupMocks(*args[:-1]) + self.options.output_file_path = None + + with self.assertRaises(AdminError) as e: + do_attestation(self.options) + self.assertIn("output file", str(e.exception)) + + self.get_ud_value_for_attestation.assert_not_called() + self.get_hsm.assert_not_called() + self.hsm.get_powhsm_attestation.assert_not_called() + self.do_unlock.assert_not_called() + self.HSMCertificateV2.assert_not_called() + self.HSMCertificateV2Element.assert_not_called() + + def test_adm_err(self, *args): + self.setupMocks(*args[:-1]) + + self.hsm.get_powhsm_attestation.side_effect = RuntimeError("an error") + + with self.assertRaises(RuntimeError) as e: + do_attestation(self.options) + self.assertIn("an error", str(e.exception)) + + self.get_ud_value_for_attestation.assert_called_with("an-ud-source") + self.do_unlock.assert_called_with(self.options, label=False) + self.get_hsm.assert_called_with("is-verbose") + self.hsm.get_powhsm_attestation.assert_called_with("some-random-value") + self.hsm.disconnect.assert_not_called() + self.HSMCertificateV2.assert_not_called() + self.HSMCertificateV2Element.assert_not_called() diff --git a/setup.cfg b/setup.cfg index bd38e3cf..b11ff18e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,9 @@ max-line-length = 90 indent-size = 4 # Ignore unused imports in __init__.py files -per-file-ignores = __init__.py:F401 +per-file-ignores = + __init__.py:F401, + middleware/admin/certificate.py:F401, show-source = False statistics = True From 5c5396711f53ace7448616ac811a90bb9a3c5fd8 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Tue, 17 Dec 2024 02:29:32 +1300 Subject: [PATCH 09/21] Certificate V2 generator (#229) - Added individual certificate v2 element types with serialization logic: SgxQuote, SgxAttestationKey and X509 - Added CStruct abstraction to simplify representing C structs - Added struct definitions as CStruct subtypes based on OpenEnclave's endorsement type definitions - Updated SGX attestation gathering logic to parse the envelope and generate the actual certificate elements - Added and updated unit tests - Ignoring long line linting in envelope unit tests --- middleware/admin/certificate.py | 4 +- middleware/admin/certificate_v2.py | 60 ++++- middleware/admin/sgx_attestation.py | 74 +++++- middleware/comm/cstruct.py | 163 +++++++++++++ middleware/sgx/envelope.py | 192 +++++++++++++++ middleware/tests/admin/test_certificate_v1.py | 2 +- middleware/tests/admin/test_certificate_v2.py | 91 +++++++ .../tests/admin/test_sgx_attestation.py | 130 +++++++++- middleware/tests/comm/test_cstruct.py | 223 ++++++++++++++++++ middleware/tests/sgx/test_envelope.py | 112 +++++++++ setup.cfg | 1 + 11 files changed, 1026 insertions(+), 26 deletions(-) create mode 100644 middleware/comm/cstruct.py create mode 100644 middleware/sgx/envelope.py create mode 100644 middleware/tests/admin/test_certificate_v2.py create mode 100644 middleware/tests/comm/test_cstruct.py create mode 100644 middleware/tests/sgx/test_envelope.py diff --git a/middleware/admin/certificate.py b/middleware/admin/certificate.py index 64b3bb6b..1b86f791 100644 --- a/middleware/admin/certificate.py +++ b/middleware/admin/certificate.py @@ -21,4 +21,6 @@ # SOFTWARE. from .certificate_v1 import HSMCertificate, HSMCertificateElement -from .certificate_v2 import HSMCertificateV2, HSMCertificateV2Element +from .certificate_v2 import HSMCertificateV2, HSMCertificateV2ElementSGXQuote, \ + HSMCertificateV2ElementSGXAttestationKey, \ + HSMCertificateV2ElementX509 diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index 61e74ce5..962fa3a0 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -24,16 +24,62 @@ class HSMCertificateV2Element: - # TODO: actual logic and subclasses - def __init__(self, element_map): - self.element_map = element_map + pass - # Stub - def name(self): - return "attestation" + +class HSMCertificateV2ElementSGXQuote(HSMCertificateV2Element): + def __init__(self, name, message, custom_data, signature, signed_by): + self.name = name + self.message = message + self.custom_data = custom_data + self.signature = signature + self.signed_by = signed_by + + def to_dict(self): + return { + "name": self.name, + "type": "sgx_quote", + "message": self.message.hex(), + "custom_data": self.custom_data.hex(), + "signature": self.signature.hex(), + "signed_by": self.signed_by, + } + + +class HSMCertificateV2ElementSGXAttestationKey(HSMCertificateV2Element): + def __init__(self, name, message, key, auth_data, signature, signed_by): + self.name = name + self.message = message + self.key = key + self.auth_data = auth_data + self.signature = signature + self.signed_by = signed_by + + def to_dict(self): + return { + "name": self.name, + "type": "sgx_attestation_key", + "message": self.message.hex(), + "key": self.key.hex(), + "auth_data": self.auth_data.hex(), + "signature": self.signature.hex(), + "signed_by": self.signed_by, + } + + +class HSMCertificateV2ElementX509(HSMCertificateV2Element): + def __init__(self, name, message, signed_by): + self.name = name + self.message = message + self.signed_by = signed_by def to_dict(self): - return self.element_map + return { + "name": self.name, + "type": "x509_pem", + "message": self.message.decode('ASCII'), + "signed_by": self.signed_by, + } class HSMCertificateV2(HSMCertificate): diff --git a/middleware/admin/sgx_attestation.py b/middleware/admin/sgx_attestation.py index c261f0ba..364df5b2 100644 --- a/middleware/admin/sgx_attestation.py +++ b/middleware/admin/sgx_attestation.py @@ -20,9 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import ecdsa from .misc import info, head, get_hsm, AdminError, get_ud_value_for_attestation from .unlock import do_unlock -from .certificate import HSMCertificateV2, HSMCertificateV2Element +from sgx.envelope import SgxEnvelope +from .certificate import HSMCertificateV2, HSMCertificateV2ElementSGXQuote, \ + HSMCertificateV2ElementSGXAttestationKey, \ + HSMCertificateV2ElementX509 def do_attestation(options): @@ -58,14 +62,72 @@ def do_attestation(options): hsm.disconnect() + # Parse envelope + info("Parsing the powHSM attestation envelope...") + try: + envelope = SgxEnvelope( + bytes.fromhex(powhsm_attestation["envelope"]), + bytes.fromhex(powhsm_attestation["message"])) + except Exception as e: + raise AdminError(f"SGX envelope parse error: {str(e)}") + + # Conversions + quote_signature = ecdsa.util.sigdecode_string( + envelope.quote_auth_data.signature.r + + envelope.quote_auth_data.signature.s, + ecdsa.NIST256p.order) + quote_signature = ecdsa.util.sigencode_der( + quote_signature[0], + quote_signature[1], + ecdsa.NIST256p.order) + att_key = ecdsa.VerifyingKey.from_string( + envelope.quote_auth_data.attestation_key.x + + envelope.quote_auth_data.attestation_key.y, + ecdsa.NIST256p) + qe_rb_signature = ecdsa.util.sigdecode_string( + envelope.quote_auth_data.qe_report_body_signature.r + + envelope.quote_auth_data.qe_report_body_signature.s, + ecdsa.NIST256p.order) + qe_rb_signature = ecdsa.util.sigencode_der( + qe_rb_signature[0], + qe_rb_signature[1], + ecdsa.NIST256p.order) + # Generate and save the attestation certificate info("Generating the attestation certificate... ", options.verbose) - att_cert = HSMCertificateV2() - # TODO: - # 1. Parse envelope - # 2. Add actual elements of the certificate - att_cert.add_element(HSMCertificateV2Element(powhsm_attestation)) + + att_cert.add_element( + HSMCertificateV2ElementSGXQuote( + name="quote", + message=envelope.quote.get_raw_data(), + custom_data=envelope.custom_message, + signature=quote_signature, + signed_by="attestation", + )) + att_cert.add_element( + HSMCertificateV2ElementSGXAttestationKey( + name="attestation", + message=envelope.quote_auth_data.qe_report_body.get_raw_data(), + key=att_key.to_string("uncompressed"), + auth_data=envelope.qe_auth_data.data, + signature=qe_rb_signature, + signed_by="quoting_enclave", + )) + att_cert.add_element( + HSMCertificateV2ElementX509( + name="quoting_enclave", + message=envelope.qe_cert_data.certs[0], + signed_by="platform_ca", + )) + att_cert.add_element( + HSMCertificateV2ElementX509( + name="platform_ca", + message=envelope.qe_cert_data.certs[1], + signed_by="sgx_root", + )) + + att_cert.add_target("quote") att_cert.save_to_jsonfile(options.output_file_path) info(f"Attestation certificate saved to {options.output_file_path}") diff --git a/middleware/comm/cstruct.py b/middleware/comm/cstruct.py new file mode 100644 index 00000000..77203988 --- /dev/null +++ b/middleware/comm/cstruct.py @@ -0,0 +1,163 @@ +# 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 struct +import re + + +class CStruct: + MAP = { + "uint8_t": ["B", "s"], + "uint16_t": "H", + "uint32_t": "I", + "uint64_t": "Q", + } + SPEC = None + TYPENAME = None + + @classmethod + def _spec(cls, little=True): + if cls.SPEC is None or little not in cls.SPEC: + fmt = "<" if little else ">" + atrmap = {} + names = [] + types = [] + index = 0 + typename = None + for line in cls.__doc__.split("\n")[1:]: + line = re.sub(r"\s+", " ", line.strip()) + if line == "": + continue + if typename is None: + typename = line + continue + tspec = line.split(" ") + + length = "" if len(tspec) < 3 else str(int(tspec[2].strip(), 10)) + + typ = tspec[0].strip() + actual_type = None + derived_type = None + if typ not in cls.MAP.keys(): + for kls in cls.__base__.__subclasses__(): + if cls != kls: + if typ == kls._typename(): + actual_type = kls + derived_type = kls + if derived_type is None: + raise ValueError(f"Invalid type: {typ}") + else: + actual_type = cls.MAP[typ] + + if length != "" and not isinstance(actual_type, list): + raise ValueError(f"Invalid type spec: {line}") + + name = tspec[1].strip() + + if isinstance(actual_type, list): + actual_type = actual_type[0] if length == "" else actual_type[1] + elif not isinstance(actual_type, str) and \ + issubclass(actual_type, cls.__base__): + actual_type = str(actual_type.get_bytelength(little)) + "s" + + fmt += length + actual_type + names.append(name) + types.append(derived_type) + atrmap[name] = index + index += 1 + if cls.SPEC is None: + cls.SPEC = {} + cls.SPEC[little] = (struct.Struct(fmt), atrmap, names, types, typename) + + return cls.SPEC[little] + + @classmethod + def _struct(cls, little=True): + return cls._spec(little)[0] + + @classmethod + def _atrmap(cls, little=True): + return cls._spec(little)[1] + + @classmethod + def _names(cls, little=True): + return cls._spec(little)[2] + + @classmethod + def _types(cls, little=True): + return cls._spec(little)[3] + + @classmethod + def _typename(cls): + if cls.TYPENAME is None: + for line in cls.__doc__.split("\n"): + line = re.sub(r"\s+", " ", line.strip()) + if line == "": + continue + cls.TYPENAME = line + break + + return cls.TYPENAME + + @classmethod + def get_bytelength(cls, little=True): + return cls._struct(little).size + + def __init__(self, value, offset=0, little=True): + self._offset = offset + self._little = little + self._raw_value = value + + try: + self._parsed = list(self._struct(little).unpack_from(value, offset)) + except Exception as e: + raise ValueError(f"While parsing: {e}") + + for index, derived_type in enumerate(self._types(little)): + if derived_type is not None: + self._parsed[index] = derived_type(self._parsed[index], little=little) + + def _value(self, name): + amap = self._atrmap(self._little) + if name in amap: + return self._parsed[amap[name]] + raise NameError(f"Property {name} does not exist") + + def __getattr__(self, name): + return self._value(name) + + def get_raw_data(self): + return self._raw_value[ + self._offset:self._offset+self.get_bytelength(self._little)] + + def to_dict(self): + result = {} + for name in self._names(self._little): + value = self._value(name) + if isinstance(value, bytes): + value = value.hex() + result[name] = value.to_dict() if isinstance(value, CStruct) else value + return result + + def __repr__(self): + return f"<{self.__class__.__name__}: {self.to_dict()}>" diff --git a/middleware/sgx/envelope.py b/middleware/sgx/envelope.py new file mode 100644 index 00000000..39f86aab --- /dev/null +++ b/middleware/sgx/envelope.py @@ -0,0 +1,192 @@ +# 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. + +from comm.cstruct import CStruct + + +class SgxEnvelope(CStruct): + """ + sgx_envelope_t + + sgx_quote_t quote + sgx_quote_tail_t quote_tail + sgx_quote_auth_data_t quote_auth_data + """ + + def __init__(self, envelope_bytes, custom_message_bytes, offset=0, little=True): + super().__init__(envelope_bytes, offset, little) + offset += self.get_bytelength() + + qead = SgxQeAuthData(envelope_bytes, offset, little) + offset += qead.get_total_bytelength() + self.qe_auth_data = qead + + qecd = SgxQeCertData(envelope_bytes, offset, little) + offset += qecd.get_total_bytelength() + self.qe_cert_data = qecd + + if envelope_bytes[offset:] != custom_message_bytes: + raise ValueError("Unexpected custom message in envelope tail") + self.custom_message = custom_message_bytes + +############################################################################## +# Types below taken from OpenEnclave's include/openenclave/bits/sgx/sgxtypes.h +############################################################################## + + +class SgxAttributes(CStruct): + """ + sgx_attributes_t + + uint64_t flags + uint64_t xfrm + """ + + +class SgxReportData(CStruct): + """ + sgx_report_data_t + + uint8_t field 64 + """ + + +class SgxReportBody(CStruct): + """ + sgx_report_body_t + + uint8_t cpusvn 16 + uint32_t miscselect + uint8_t reserved1 12 + uint8_t isvextprodid 16 + sgx_attributes_t attributes + uint8_t mrenclave 32 + uint8_t reserved2 32 + uint8_t mrsigner 32 + uint8_t reserved3 32 + uint8_t configid 64 + uint16_t isvprodid + uint16_t isvsvn + uint16_t configsvn + uint8_t reserved4 42 + uint8_t isvfamilyid 16 + sgx_report_data_t report_data + """ + + +class SgxEcdsa256Signature(CStruct): + """ + sgx_ecdsa256_signature_t + + uint8_t r 32 + uint8_t s 32 + """ + + +class SgxEcdsa256Key(CStruct): + """ + sgx_ecdsa256_key_t + + uint8_t x 32 + uint8_t y 32 + """ + + +class SgxQuote(CStruct): + """ + sgx_quote_t + + uint16_t version + uint16_t sign_type + uint32_t tee_type + uint16_t qe_svn + uint16_t pce_svn + uint8_t uuid 16 + uint8_t user_data 20 + sgx_report_body_t report_body + """ + + +# This is actually part of sgx_quote_t, separated +# for pratical reasons since the signature doesn't include +# this field +class SgxQuoteTail(CStruct): + """ + sgx_quote_tail_t + + uint32_t signature_len + """ + + +class SgxQuoteAuthData(CStruct): + """ + sgx_quote_auth_data_t + + sgx_ecdsa256_signature_t signature + sgx_ecdsa256_key_t attestation_key + sgx_report_body_t qe_report_body + sgx_ecdsa256_signature_t qe_report_body_signature + """ + +#################################################################### +# The following two structs are augmented with content parsing logic +#################################################################### + + +class SgxQeAuthData(CStruct): + """ + sgx_qe_auth_data_t + + uint16_t size + """ + + def __init__(self, value, offset=0, little=True): + super().__init__(value, offset, little) + os = offset + self.get_bytelength() + data = value[os:os+self.size] + if len(data) != self.size: + raise ValueError(f"Expected {self.size} data bytes but only got {len(data)}") + self.data = data + + def get_total_bytelength(self): + return self.get_bytelength() + len(self.data) + + +class SgxQeCertData(SgxQeAuthData): + """ + sgx_qe_cert_data_t + + uint16_t type + uint32_t size + """ + SPEC = None + TYPENAME = None + + X509_START_MARKER = b"-----BEGIN CERTIFICATE-----\n" + X509_END_MARKER = b"\n-----END CERTIFICATE-----\n" + + def __init__(self, value, offset=0, little=True): + super().__init__(value, offset, little) + self.certs = list(map(lambda c: c.replace(self.X509_START_MARKER, b""), + filter(lambda c: + c.strip().startswith(self.X509_START_MARKER), + self.data.split(self.X509_END_MARKER)))) diff --git a/middleware/tests/admin/test_certificate_v1.py b/middleware/tests/admin/test_certificate_v1.py index 591cfc91..f7503bba 100644 --- a/middleware/tests/admin/test_certificate_v1.py +++ b/middleware/tests/admin/test_certificate_v1.py @@ -26,7 +26,7 @@ from unittest import TestCase from unittest.mock import call, patch, mock_open -from admin.certificate import HSMCertificate, HSMCertificateElement +from admin.certificate_v1 import HSMCertificate, HSMCertificateElement class TestHSMCertificate(TestCase): diff --git a/middleware/tests/admin/test_certificate_v2.py b/middleware/tests/admin/test_certificate_v2.py new file mode 100644 index 00000000..f4a16da6 --- /dev/null +++ b/middleware/tests/admin/test_certificate_v2.py @@ -0,0 +1,91 @@ +# 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. + +from unittest import TestCase +from admin.certificate_v1 import HSMCertificate +from admin.certificate_v2 import HSMCertificateV2, HSMCertificateV2ElementSGXQuote, \ + HSMCertificateV2ElementSGXAttestationKey, \ + HSMCertificateV2ElementX509 + + +class TestHSMCertificateV2(TestCase): + def test_behavior_inherited(self): + self.assertTrue(issubclass(HSMCertificateV2, HSMCertificate)) + + def test_create_empty_certificate_ok(self): + cert = HSMCertificateV2() + self.assertEqual({'version': 2, 'targets': [], 'elements': []}, cert.to_dict()) + + +class TestHSMCertificateV2ElementSGXQuote(TestCase): + def test_dict_ok(self): + elem = HSMCertificateV2ElementSGXQuote( + "thename", + bytes.fromhex("aabbcc"), + bytes.fromhex("ddeeff"), + bytes.fromhex("112233"), + "whosigned" + ) + self.assertEqual({ + "name": "thename", + "type": "sgx_quote", + "message": "aabbcc", + "custom_data": "ddeeff", + "signature": "112233", + "signed_by": "whosigned", + }, elem.to_dict()) + + +class TestHSMCertificateV2ElementSGXAttestationKey(TestCase): + def test_dict_ok(self): + elem = HSMCertificateV2ElementSGXAttestationKey( + "thename", + bytes.fromhex("aabbcc"), + bytes.fromhex("ddeeff"), + bytes.fromhex("112233"), + bytes.fromhex("44556677"), + "whosigned" + ) + self.assertEqual({ + "name": "thename", + "type": "sgx_attestation_key", + "message": "aabbcc", + "key": "ddeeff", + "auth_data": "112233", + "signature": "44556677", + "signed_by": "whosigned", + }, elem.to_dict()) + + +class TestHSMCertificateV2ElementX509(TestCase): + def test_dict_ok(self): + elem = HSMCertificateV2ElementX509( + "thename", + b"this is an ascii message", + "whosigned" + ) + self.assertEqual({ + "name": "thename", + "type": "x509_pem", + "message": "this is an ascii message", + "signed_by": "whosigned", + }, elem.to_dict()) diff --git a/middleware/tests/admin/test_sgx_attestation.py b/middleware/tests/admin/test_sgx_attestation.py index 2f05f2da..4b60fadb 100644 --- a/middleware/tests/admin/test_sgx_attestation.py +++ b/middleware/tests/admin/test_sgx_attestation.py @@ -20,17 +20,21 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import ecdsa from types import SimpleNamespace from unittest import TestCase -from unittest.mock import Mock, patch +from unittest.mock import Mock, patch, call from parameterized import parameterized from admin.sgx_attestation import do_attestation from admin.misc import AdminError @patch("sys.stdout") -@patch("admin.sgx_attestation.HSMCertificateV2Element") +@patch("admin.sgx_attestation.HSMCertificateV2ElementX509") +@patch("admin.sgx_attestation.HSMCertificateV2ElementSGXAttestationKey") +@patch("admin.sgx_attestation.HSMCertificateV2ElementSGXQuote") @patch("admin.sgx_attestation.HSMCertificateV2") +@patch("admin.sgx_attestation.SgxEnvelope") @patch("admin.sgx_attestation.do_unlock") @patch("admin.sgx_attestation.get_ud_value_for_attestation") @patch("admin.sgx_attestation.get_hsm") @@ -44,15 +48,56 @@ def setUp(self): self.options = options def setupMocks(self, get_hsm, get_ud_value_for_attestation, do_unlock, - HSMCertificateV2, HSMCertificateV2Element): + SgxEnvelope, HSMCertificateV2, HSMCertificateV2ElementSGXQuote, + HSMCertificateV2ElementSGXAttestationKey, HSMCertificateV2ElementX509): self.get_hsm = get_hsm self.get_ud_value_for_attestation = get_ud_value_for_attestation self.do_unlock = do_unlock + self.SgxEnvelope = SgxEnvelope self.HSMCertificateV2 = HSMCertificateV2 - self.HSMCertificateV2Element = HSMCertificateV2Element + self.HSMCertificateV2ElementSGXQuote = HSMCertificateV2ElementSGXQuote + self.HSMCertificateV2ElementSGXAttestationKey = \ + HSMCertificateV2ElementSGXAttestationKey + self.HSMCertificateV2ElementX509 = HSMCertificateV2ElementX509 self.hsm = Mock() - self.hsm.get_powhsm_attestation.return_value = "the-attestation" + self.hsm.get_powhsm_attestation.return_value = { + "envelope": "11"*32, + "message": "22"*32, + } + quote = SimpleNamespace(**{"get_raw_data": lambda: "quote-raw-data"}) + sig = SimpleNamespace(**{"r": b"a"*32, "s": b"a"*32}) + self.att_key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p).\ + get_verifying_key() + att_key_str = self.att_key.to_string() + attkey = SimpleNamespace(**{"x": att_key_str[:32], "y": att_key_str[32:]}) + qesig = SimpleNamespace(**{"r": b"c"*32, "s": b"c"*32}) + qad = SimpleNamespace(**{ + "signature": sig, + "attestation_key": attkey, + "qe_report_body": SimpleNamespace(**{ + "get_raw_data": lambda: "qerb-raw-data"}), + "qe_report_body_signature": qesig, + }) + qead = SimpleNamespace(**{ + "data": "qead-data", + }) + qecd = SimpleNamespace(**{ + "certs": ["qecd-cert-0", "qecd-cert-1"], + }) + envelope = SimpleNamespace(**{ + "quote": quote, + "quote_auth_data": qad, + "qe_auth_data": qead, + "qe_cert_data": qecd, + "custom_message": "a-custom-message", + }) + self.SgxEnvelope.return_value = envelope + + self.HSMCertificateV2ElementSGXQuote.return_value = "quote_elem" + self.HSMCertificateV2ElementSGXAttestationKey.return_value = "attkey_elem" + self.HSMCertificateV2ElementX509.side_effect = ["cert0_elem", "cert1_elem"] + get_hsm.return_value = self.hsm get_ud_value_for_attestation.return_value = "some-random-value" @@ -74,10 +119,44 @@ def test_ok(self, *args): self.get_hsm.assert_called_with("is-verbose") self.hsm.get_powhsm_attestation.assert_called_with("some-random-value") self.hsm.disconnect.assert_called() - self.HSMCertificateV2Element.assert_called_with("the-attestation") - elem = self.HSMCertificateV2Element.return_value + self.SgxEnvelope.assert_called_with( + bytes.fromhex("11"*32), + bytes.fromhex("22"*32), + ) + self.HSMCertificateV2ElementSGXQuote.assert_called_with( + name="quote", + message="quote-raw-data", + custom_data="a-custom-message", + signature=bytes.fromhex("30440220"+"61"*32+"0220"+"61"*32), + signed_by="attestation", + ) + self.HSMCertificateV2ElementSGXAttestationKey.assert_called_with( + name="attestation", + message="qerb-raw-data", + key=self.att_key.to_string("uncompressed"), + auth_data="qead-data", + signature=bytes.fromhex("30440220"+"63"*32+"0220"+"63"*32), + signed_by="quoting_enclave", + ) + self.HSMCertificateV2ElementX509.assert_has_calls([ + call( + name="quoting_enclave", + message="qecd-cert-0", + signed_by="platform_ca", + ), + call( + name="platform_ca", + message="qecd-cert-1", + signed_by="sgx_root", + ) + ]) cert = self.HSMCertificateV2.return_value - cert.add_element.assert_called_with(elem) + cert.add_element.assert_has_calls([ + call("quote_elem"), + call("attkey_elem"), + call("cert0_elem"), + call("cert1_elem") + ]) cert.save_to_jsonfile.assert_called_with("an-output-file") def test_no_output_path(self, *args): @@ -92,10 +171,13 @@ def test_no_output_path(self, *args): self.get_hsm.assert_not_called() self.hsm.get_powhsm_attestation.assert_not_called() self.do_unlock.assert_not_called() + self.SgxEnvelope.assert_not_called() self.HSMCertificateV2.assert_not_called() - self.HSMCertificateV2Element.assert_not_called() + self.HSMCertificateV2ElementSGXQuote.assert_not_called() + self.HSMCertificateV2ElementSGXAttestationKey.assert_not_called() + self.HSMCertificateV2ElementX509.assert_not_called() - def test_adm_err(self, *args): + def test_adm_err_get_attestation(self, *args): self.setupMocks(*args[:-1]) self.hsm.get_powhsm_attestation.side_effect = RuntimeError("an error") @@ -109,5 +191,31 @@ def test_adm_err(self, *args): self.get_hsm.assert_called_with("is-verbose") self.hsm.get_powhsm_attestation.assert_called_with("some-random-value") self.hsm.disconnect.assert_not_called() + self.SgxEnvelope.assert_not_called() + self.HSMCertificateV2.assert_not_called() + self.HSMCertificateV2ElementSGXQuote.assert_not_called() + self.HSMCertificateV2ElementSGXAttestationKey.assert_not_called() + self.HSMCertificateV2ElementX509.assert_not_called() + + def test_adm_err_envelope_parsing(self, *args): + self.setupMocks(*args[:-1]) + + self.SgxEnvelope.side_effect = ValueError("an error") + + with self.assertRaises(AdminError) as e: + do_attestation(self.options) + self.assertIn("envelope parse error", str(e.exception)) + + self.get_ud_value_for_attestation.assert_called_with("an-ud-source") + self.do_unlock.assert_called_with(self.options, label=False) + self.get_hsm.assert_called_with("is-verbose") + self.hsm.get_powhsm_attestation.assert_called_with("some-random-value") + self.hsm.disconnect.assert_called() + self.SgxEnvelope.assert_called_with( + bytes.fromhex("11"*32), + bytes.fromhex("22"*32), + ) self.HSMCertificateV2.assert_not_called() - self.HSMCertificateV2Element.assert_not_called() + self.HSMCertificateV2ElementSGXQuote.assert_not_called() + self.HSMCertificateV2ElementSGXAttestationKey.assert_not_called() + self.HSMCertificateV2ElementX509.assert_not_called() diff --git a/middleware/tests/comm/test_cstruct.py b/middleware/tests/comm/test_cstruct.py new file mode 100644 index 00000000..8c04d47d --- /dev/null +++ b/middleware/tests/comm/test_cstruct.py @@ -0,0 +1,223 @@ +# 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. + +from unittest import TestCase +from parameterized import parameterized +from comm.cstruct import CStruct + +import logging + +logging.disable(logging.CRITICAL) + + +class RandomBisStruct(CStruct): + """ + random_bis_t + + uint16_t another_double + uint8_t single_arr 10 + uint32_t another_quad + """ + + +class RandomTrisStruct(CStruct): + """ + random_tris_t + + uint8_t arr_one 2 + uint8_t arr_two 3 + """ + + +class RandomStruct(CStruct): + """ + random_t + + uint8_t single_val + uint16_t double_val + uint32_t quad_val + uint64_t oct_val + + random_bis_t other_random + random_tris_t yet_other_random + """ + + +class Invalid1(CStruct): + """ + invalid_1 + + nonexistent_type something + """ + + +class Invalid2(CStruct): + """ + invalid_2 + + uint32_t withlength 5 + """ + + +class ValidWithInvalid(CStruct): + """ + valid_with_invalid + + uint8_t a_number + uint16_t another_number + invalid_2 something_invalid + """ + + +class TestCStruct(TestCase): + def setUp(self): + self.packed = bytes.fromhex( + "99" # single_val + "0102" # double_val + "03040506" # quad_val + "0708090a0b0c0d0e" # oct_val + "8899" # another_double + "00112233445566778899" # single_arr + "d1d2d3d4" # another_quad + "aabb" # arr_one + "ccddee" # arr_two + ) + + def test_expected_sizes(self): + self.assertEqual(16, RandomBisStruct.get_bytelength()) + self.assertEqual(5, RandomTrisStruct.get_bytelength()) + self.assertEqual(15 + + RandomBisStruct.get_bytelength() + + RandomTrisStruct.get_bytelength(), + RandomStruct.get_bytelength()) + + def test_parsing_default(self): + parsed = RandomStruct(self.packed) + + self.assertEqual(0x99, parsed.single_val) + self.assertEqual(0x0201, parsed.double_val) + self.assertEqual(0x06050403, parsed.quad_val) + self.assertEqual(0x0e0d0c0b0a090807, parsed.oct_val) + + self.assertEqual(0x9988, parsed.other_random.another_double) + self.assertEqual(bytes.fromhex("00112233445566778899"), + parsed.other_random.single_arr) + self.assertEqual(0xd4d3d2d1, parsed.other_random.another_quad) + + self.assertEqual(bytes.fromhex("aabb"), parsed.yet_other_random.arr_one) + self.assertEqual(bytes.fromhex("ccddee"), parsed.yet_other_random.arr_two) + + self.assertEqual({ + "single_val": 0x99, + "double_val": 0x0201, + "quad_val": 0x06050403, + "oct_val": 0x0e0d0c0b0a090807, + "other_random": { + "another_double": 0x9988, + "single_arr": "00112233445566778899", + "another_quad": 0xd4d3d2d1, + }, + "yet_other_random": { + "arr_one": "aabb", + "arr_two": "ccddee", + } + }, parsed.to_dict()) + + def test_parsing_little_offset(self): + parsed = RandomStruct(b"thisisrandom" + self.packed, offset=12, little=True) + + self.assertEqual(0x99, parsed.single_val) + self.assertEqual(0x0201, parsed.double_val) + self.assertEqual(0x06050403, parsed.quad_val) + self.assertEqual(0x0e0d0c0b0a090807, parsed.oct_val) + + self.assertEqual(0x9988, parsed.other_random.another_double) + self.assertEqual(bytes.fromhex("00112233445566778899"), + parsed.other_random.single_arr) + self.assertEqual(0xd4d3d2d1, parsed.other_random.another_quad) + + self.assertEqual(bytes.fromhex("aabb"), parsed.yet_other_random.arr_one) + self.assertEqual(bytes.fromhex("ccddee"), parsed.yet_other_random.arr_two) + + self.assertEqual({ + "single_val": 0x99, + "double_val": 0x0201, + "quad_val": 0x06050403, + "oct_val": 0x0e0d0c0b0a090807, + "other_random": { + "another_double": 0x9988, + "single_arr": "00112233445566778899", + "another_quad": 0xd4d3d2d1, + }, + "yet_other_random": { + "arr_one": "aabb", + "arr_two": "ccddee", + } + }, parsed.to_dict()) + + def test_parsing_big(self): + parsed = RandomStruct(self.packed, little=False) + + self.assertEqual(0x99, parsed.single_val) + self.assertEqual(0x0102, parsed.double_val) + self.assertEqual(0x03040506, parsed.quad_val) + self.assertEqual(0x0708090a0b0c0d0e, parsed.oct_val) + + self.assertEqual(0x8899, parsed.other_random.another_double) + self.assertEqual(bytes.fromhex("00112233445566778899"), + parsed.other_random.single_arr) + self.assertEqual(0xd1d2d3d4, parsed.other_random.another_quad) + + self.assertEqual(bytes.fromhex("aabb"), parsed.yet_other_random.arr_one) + self.assertEqual(bytes.fromhex("ccddee"), parsed.yet_other_random.arr_two) + + self.assertEqual({ + "single_val": 0x99, + "double_val": 0x0102, + "quad_val": 0x03040506, + "oct_val": 0x0708090a0b0c0d0e, + "other_random": { + "another_double": 0x8899, + "single_arr": "00112233445566778899", + "another_quad": 0xd1d2d3d4, + }, + "yet_other_random": { + "arr_one": "aabb", + "arr_two": "ccddee", + } + }, parsed.to_dict()) + + def test_parsing_toosmall(self): + with self.assertRaises(ValueError): + RandomStruct(b"thisistoosmall") + + @parameterized.expand([ + ("invalid_one", Invalid1), + ("invalid_two", Invalid2), + ("valid_with_invalid", ValidWithInvalid) + ]) + def test_invalid_spec(self, _, kls): + with self.assertRaises(ValueError): + kls.get_bytelength() + + with self.assertRaises(ValueError): + kls(b'somethingtoparse') diff --git a/middleware/tests/sgx/test_envelope.py b/middleware/tests/sgx/test_envelope.py new file mode 100644 index 00000000..132a2861 --- /dev/null +++ b/middleware/tests/sgx/test_envelope.py @@ -0,0 +1,112 @@ +# 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. + +from unittest import TestCase +from parameterized import parameterized +from sgx.envelope import SgxAttributes, \ + SgxReportData, \ + SgxReportBody, \ + SgxEcdsa256Signature, \ + SgxEcdsa256Key, \ + SgxQuote, \ + SgxQuoteTail, \ + SgxQuoteAuthData, \ + SgxQeCertData, \ + SgxQeAuthData, \ + SgxEnvelope + +import logging + +logging.disable(logging.CRITICAL) + +TEST_ENVELOPE = """ +03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34d562f4564fc182000000000e0e100fffff01000000000000000000010000000000000000000000000000000000000000000000000000000000000005000000000000000700000000000000d32688d3c1f3dfcc8b0b36eac7c89d49af331800bd56248044166fa6699442c10000000000000000000000000000000000000000000000000000000000000000718c2f1a0efbd513e016fafd6cf62a624442f2d83708d4b33ab5a8d8c1cd4dd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95bb875c1a728071f70ad8c9d03f1744c19acb0580921e611ac9104f7701d00000000000000000000000000000000000000000000000000000000000000000ca100000e52b03a7bd6b5dd9feeeb375bd597730d2872643b47aff4dd641c5c3a2b8016ebbd227f67e7c23bbddeb4f8fddee031a2b961501d1c28dda082669d7ac861e6ca024cb34c90ea6a8f9f2181c9020cbcc7c073e69981733c8deed6f6c451822aa08376350ff7da01f842bb40c631cbb711f8b6f7a4fae398320a3884774d250ad0e0e100fffff0100000000000000000000000000000000000000000000000000000000000000000000000000000000001500000000000000e70000000000000096b347a64e5a045e27369c26e6dcda51fd7c850e9b3a3a79e718f43261dee1e400000000000000000000000000000000000000000000000000000000000000008c4f5775d796503e96137f77c68a829a0056ac8ded70140b081b094490c57bff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001fe721d0322954821589237fd27efb8fef1acb3ecd6b0352c31271550fc70f9400000000000000000000000000000000000000000000000000000000000000001f14d532274c4385fc0019ca2a21e53e17143cb62377ca4fcdd97fa9fef8fb2595d4ee272cf3c512e36779de67dc7814982f1160d981d138a32b265e928a05622000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0500620e00002d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494945387a4343424a69674177494241674955667232646c774e343244425541394358496b426c4750327656334177436759494b6f5a497a6a3045417749770a634445694d434147413155454177775a535735305a577767553064594946424453794251624746305a6d397962534244515445614d42674741315545436777520a535735305a577767513239796347397959585270623234784644415342674e564241634d43314e68626e526849454e7359584a684d51737743515944565151490a44414a445154454c4d416b474131554542684d4356564d774868634e4d6a51774d7a497a4d4451304e6a49785768634e4d7a45774d7a497a4d4451304e6a49780a576a42774d534977494159445651514444426c4a626e526c624342545231676755454e4c49454e6c636e52705a6d6c6a5958526c4d526f77474159445651514b0a4442464a626e526c6243424462334a7762334a6864476c76626a45554d424947413155454277774c553246756447456751327868636d4578437a414a42674e560a4241674d416b4e424d517377435159445651514745774a56557a425a4d424d4742797147534d34394167454743437147534d34394177454841304941424b6c370a52444e6c735a4b6b45744163573753664358314a656762764771344f3072525574307a2f4736665a4a734e6c706d527754423444596b72676b6d31742b3952700a4c77784658392f6b6768786944516d306a716d6a67674d4f4d494944436a416642674e5648534d4547444157674253566231334e765276683655424a796454300a4d383442567776655644427242674e56485238455a4442694d47436758714263686c706f64485277637a6f764c32467761533530636e567a6447566b633256790a646d6c6a5a584d75615735305a577775593239744c334e6e6543396a5a584a3061575a7059324630615739754c3359304c33426a61324e796244396a595431770a624746305a6d397962535a6c626d4e765a476c755a7a316b5a584977485159445652304f4242594546414c4b5635444631364b6e4562535735514d39656344710a425a61484d41344741315564447745422f775145417749477744414d42674e5648524d4241663845416a41414d4949434f77594a4b6f5a496876684e415130420a424949434c444343416967774867594b4b6f5a496876684e415130424151515174744a587569515677714d34733734672b4878664b54434341575547436971470a534962345451454e41514977676746564d42414743797147534962345451454e415149424167454f4d42414743797147534962345451454e415149434167454f0a4d42414743797147534962345451454e41514944416745444d42414743797147534962345451454e41514945416745444d42454743797147534962345451454e0a41514946416749412f7a415242677371686b69472b4530424451454342674943415038774541594c4b6f5a496876684e4151304241676343415145774541594c0a4b6f5a496876684e4151304241676743415141774541594c4b6f5a496876684e4151304241676b43415141774541594c4b6f5a496876684e4151304241676f430a415141774541594c4b6f5a496876684e4151304241677343415141774541594c4b6f5a496876684e4151304241677743415141774541594c4b6f5a496876684e0a4151304241673043415141774541594c4b6f5a496876684e4151304241673443415141774541594c4b6f5a496876684e4151304241673843415141774541594c0a4b6f5a496876684e4151304241684143415141774541594c4b6f5a496876684e4151304241684543415130774877594c4b6f5a496876684e41513042416849450a4541344f4177502f2f7745414141414141414141414141774541594b4b6f5a496876684e4151304241775143414141774641594b4b6f5a496876684e415130420a4241514741474271414141414d41384743697147534962345451454e4151554b415145774867594b4b6f5a496876684e41513042426751514456652f445855560a453467656d74674f357542707644424542676f71686b69472b453042445145484d4459774541594c4b6f5a496876684e4151304242774542416638774541594c0a4b6f5a496876684e4151304242774942415141774541594c4b6f5a496876684e4151304242774d4241514177436759494b6f5a497a6a304541774944535141770a52674968414a4667663738486767544274765150585a4a782f33466d373176434f6d74383270636539314d325a41493041694541695a4d5042625a5a6d7652320a762b316d727337364a65676c44512b704b2f534c4e39346c342b6a4d3544413d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436c6a4343416a32674177494241674956414a567658633239472b487051456e4a3150517a7a674658433935554d416f4743437147534d343942414d430a4d476778476a415942674e5642414d4d45556c756447567349464e48574342536232393049454e424d526f77474159445651514b4442464a626e526c624342440a62334a7762334a6864476c76626a45554d424947413155454277774c553246756447456751327868636d4578437a414a42674e564241674d416b4e424d5173770a435159445651514745774a56557a4165467730784f4441314d6a45784d4455774d5442614677307a4d7a41314d6a45784d4455774d5442614d484178496a41670a42674e5642414d4d47556c756447567349464e4857434251513073675547786864475a76636d306751304578476a415942674e5642416f4d45556c75644756730a49454e76636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b474131554543417743513045780a437a414a42674e5642415954416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a304441516344516741454e53422f377432316c58534f0a3243757a7078773734654a423732457944476757357258437478327456544c7136684b6b367a2b5569525a436e71523770734f766771466553786c6d546c4a6c0a65546d693257597a33714f42757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f536347724442530a42674e5648523845537a424a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b633256790a646d6c6a5a584d75615735305a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e5648513445466751556c5739640a7a62306234656c4153636e553944504f4156634c336c517744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159420a4166384341514177436759494b6f5a497a6a30454177494452774177524149675873566b6930772b6936565947573355462f32327561586530594a446a3155650a6e412b546a44316169356343494359623153416d4435786b66545670766f34556f79695359787244574c6d5552344349394e4b7966504e2b0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436a7a4343416a53674177494241674955496d554d316c71644e496e7a6737535655723951477a6b6e42717777436759494b6f5a497a6a3045417749770a614445614d4267474131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e760a636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a0a42674e5642415954416c56544d423458445445344d4455794d5445774e4455784d466f58445451354d54497a4d54497a4e546b314f566f77614445614d4267470a4131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e76636e4276636d46300a615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a42674e56424159540a416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a3044415163445167414543366e45774d4449595a4f6a2f69505773437a61454b69370a314f694f534c52466857476a626e42564a66566e6b59347533496a6b4459594c304d784f346d717379596a6c42616c54565978465032734a424b357a6c4b4f420a757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f5363477244425342674e5648523845537a424a0a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b63325679646d6c6a5a584d75615735300a5a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e564851344546675155496d554d316c71644e496e7a673753560a55723951477a6b6e4271777744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159424166384341514577436759490a4b6f5a497a6a3045417749445351417752674968414f572f35516b522b533943695344634e6f6f774c7550524c735747662f59693747535839344267775477670a41694541344a306c72486f4d732b586f356f2f7358364f39515778485241765a55474f6452513763767152586171493d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a00746869732069732061206d657373616765 +""" + +TEST_MESSAGE = "746869732069732061206d657373616765" + + +class TestSgxQeAuthData(TestCase): + def test_parses_ok(self): + parsed = SgxQeAuthData(bytes.fromhex("0a00112233445566778899aa")) + self.assertEqual(10, parsed.size) + self.assertEqual(bytes.fromhex("112233445566778899aa"), parsed.data) + + def test_parses_error_tooshort(self): + with self.assertRaises(ValueError): + SgxQeAuthData(bytes.fromhex("0a0baabbcc")) + + +class TestSgxQeCertData(TestCase): + def test_parses_ok(self): + certs = \ +b""" +-----BEGIN CERTIFICATE----- +this is certificate one +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +this is certificate two +-----END CERTIFICATE----- +""" + parsed = SgxQeCertData( + bytes.fromhex("1122") + + len(certs).to_bytes(4, byteorder="little", signed=False) + + certs + ) + self.assertEqual(0x2211, parsed.type) + self.assertEqual(certs, parsed.data) + self.assertEqual(2, len(parsed.certs)) + + def test_parses_error_tooshort(self): + with self.assertRaises(ValueError): + SgxQeAuthData(bytes.fromhex("0a0baabbcc")) + + +class TestSgxEnvelope(TestCase): + def test_parses_ok(self): + envelope = SgxEnvelope( + bytes.fromhex(TEST_ENVELOPE), + bytes.fromhex(TEST_MESSAGE) + ) + + self.assertEqual(TEST_MESSAGE, envelope.custom_message.hex()) + + def test_parsing_fails_if_message_mismatch(self): + with self.assertRaises(ValueError): + SgxEnvelope(bytes.fromhex(TEST_ENVELOPE), b"some-other-message") + + +class TestSgxStructs(TestCase): + # Sizes taken from OpenEnclave's include/openenclave/bits/sgx/sgxtypes.h + # sgx_quote_t is smaller due to not including the last field (signature_len) + @parameterized.expand([ + ("sgx_attributes_t", SgxAttributes, 16), + ("sgx_report_data_t", SgxReportData, 64), + ("sgx_report_body_t", SgxReportBody, 384), + ("sgx_ecdsa256_signature_t", SgxEcdsa256Signature, 64), + ("sgx_ecdsa256_key_t", SgxEcdsa256Key, 64), + ("sgx_quote_t", SgxQuote, 432), + ("sgx_quote_tail_t", SgxQuoteTail, 4), + ("sgx_quote_auth_data_t", SgxQuoteAuthData, 576), + ]) + def test_sizes_ok(self, _, kls, exp_len): + self.assertEqual(exp_len, kls.get_bytelength()) diff --git a/setup.cfg b/setup.cfg index b11ff18e..da90c29b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ indent-size = 4 per-file-ignores = __init__.py:F401, middleware/admin/certificate.py:F401, + middleware/tests/sgx/test_envelope.py:E122, show-source = False statistics = True From 76b46833f2e6e81fa81136d7b536e3f1824f1646 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Thu, 19 Dec 2024 03:21:09 +1300 Subject: [PATCH 10/21] Certificate V2 parser (#230) - Added version mapping and parsing logic to certificate version 1 - Added dict based initialisation functions to HSMCertificateV2Element and its subclasses - Updated sgx attestation gathering certificate generation - Added and updated unit tests - Ignoring long line linting in certificate v2 resources file --- middleware/admin/certificate.py | 7 + middleware/admin/certificate_v1.py | 30 ++- middleware/admin/certificate_v2.py | 160 +++++++++--- middleware/admin/sgx_attestation.py | 50 ++-- middleware/tests/admin/test_certificate_v1.py | 4 +- middleware/tests/admin/test_certificate_v2.py | 230 ++++++++++++++++-- .../admin/test_certificate_v2_resources.py | 63 +++++ .../tests/admin/test_sgx_attestation.py | 58 ++--- setup.cfg | 1 + 9 files changed, 480 insertions(+), 123 deletions(-) create mode 100644 middleware/tests/admin/test_certificate_v2_resources.py diff --git a/middleware/admin/certificate.py b/middleware/admin/certificate.py index 1b86f791..1321a0b2 100644 --- a/middleware/admin/certificate.py +++ b/middleware/admin/certificate.py @@ -24,3 +24,10 @@ from .certificate_v2 import HSMCertificateV2, HSMCertificateV2ElementSGXQuote, \ HSMCertificateV2ElementSGXAttestationKey, \ HSMCertificateV2ElementX509 + + +# Assign version mapping to the parent class +HSMCertificate.VERSION_MAPPING = { + 1: HSMCertificate, + 2: HSMCertificateV2, +} diff --git a/middleware/admin/certificate_v1.py b/middleware/admin/certificate_v1.py index a37802ed..414fe8f3 100644 --- a/middleware/admin/certificate_v1.py +++ b/middleware/admin/certificate_v1.py @@ -125,18 +125,25 @@ class HSMCertificate: VERSION = 1 # Only supported version ROOT_ELEMENT = "root" ELEMENT_BASE_CLASS = HSMCertificateElement + ELEMENT_FACTORY = HSMCertificateElement - @staticmethod - def from_jsonfile(path): + @classmethod + def from_jsonfile(kls, path): try: with open(path, "r") as file: certificate_map = json.loads(file.read()) if type(certificate_map) != dict: raise ValueError( - "JSON file must contain an object as a top level element") + "Certificate file must contain an object as a top level element") - return HSMCertificate(certificate_map) + version = certificate_map.get("version") + if version not in kls.VERSION_MAPPING: + raise ValueError("Invalid or unsupported HSM certificate " + f"version {version} (supported versions are " + f"{", ".join(kls.VERSION_MAPPING.keys())})") + + return kls.VERSION_MAPPING[version](certificate_map) except (ValueError, json.JSONDecodeError) as e: raise ValueError('Unable to read HSM certificate from "%s": %s' % (path, str(e))) @@ -190,8 +197,8 @@ def validate_and_get_values(self, raw_root_pubkey_hex): def add_element(self, element): if not isinstance(element, self.ELEMENT_BASE_CLASS): - raise ValueError( - f"Expected an HSMCertificateElement but got a {type(element)}") + raise ValueError(f"Expected an {self.ELEMENT_BASE_CLASS.__name__} " + "but got a {type(element)}") self._elements[element.name] = element def clear_targets(self): @@ -214,11 +221,10 @@ def save_to_jsonfile(self, path): file.write("%s\n" % json.dumps(self.to_dict(), indent=2)) def _parse(self, certificate_map): - if "version" not in certificate_map or certificate_map["version"] != self.VERSION: - raise ValueError( - "Invalid or unsupported HSM certificate version " - f"(current version is {self.VERSION})" - ) + version = certificate_map.get("version") + if version != self.VERSION: + raise ValueError("Invalid or unexpected HSM certificate version " + f"{version} (expected {self.VERSION})") if "targets" not in certificate_map or type(certificate_map["targets"]) != list: raise ValueError("Missing or invalid targets") @@ -229,7 +235,7 @@ def _parse(self, certificate_map): raise ValueError("Missing elements") for item in certificate_map["elements"]: - element = HSMCertificateElement(item) + element = self.ELEMENT_FACTORY(item) self._elements[item["name"]] = element # Sanity: check each target has a path to the root authority diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index 962fa3a0..a7b58f7c 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -20,76 +20,176 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import base64 from .certificate_v1 import HSMCertificate +from .utils import is_nonempty_hex_string class HSMCertificateV2Element: - pass + def __init__(self): + raise RuntimeError("Cannot instantiate an " + "abstract HSMCertificateV2Element") + + @classmethod + def from_dict(kls, element_map): + if element_map.get("type") not in kls.TYPE_MAPPING: + raise ValueError("Invalid or missing element type for " + f"element {element_map.get("name")}") + + return kls.TYPE_MAPPING[element_map["type"]](element_map) + + def _init_with_map(self, element_map): + if "name" not in element_map: + raise ValueError("Missing name for HSM certificate element") + + self._name = element_map["name"] + + if "signed_by" not in element_map: + raise ValueError("Missing certifier for HSM certificate element") + self._signed_by = element_map["signed_by"] + + @property + def name(self): + return self._name + + @property + def signed_by(self): + return self._signed_by class HSMCertificateV2ElementSGXQuote(HSMCertificateV2Element): - def __init__(self, name, message, custom_data, signature, signed_by): - self.name = name - self.message = message - self.custom_data = custom_data - self.signature = signature - self.signed_by = signed_by + def __init__(self, element_map): + self._init_with_map(element_map) + + def _init_with_map(self, element_map): + super()._init_with_map(element_map) + + if not is_nonempty_hex_string(element_map.get("message")): + raise ValueError(f"Invalid message for HSM certificate element {self.name}") + self._message = bytes.fromhex(element_map["message"]) + + if not is_nonempty_hex_string(element_map.get("custom_data")): + raise ValueError("Invalid custom data for HSM certificate " + f"element {self.name}") + self._custom_data = bytes.fromhex(element_map["custom_data"]) + + if not is_nonempty_hex_string(element_map.get("signature")): + raise ValueError("Invalid signature for HSM certificate element {self.name}") + self._signature = bytes.fromhex(element_map["signature"]) + + @property + def message(self): + return self._message.hex() + + @property + def custom_data(self): + return self._custom_data.hex() + + @property + def signature(self): + return self._signature.hex() def to_dict(self): return { "name": self.name, "type": "sgx_quote", - "message": self.message.hex(), - "custom_data": self.custom_data.hex(), - "signature": self.signature.hex(), + "message": self.message, + "custom_data": self.custom_data, + "signature": self.signature, "signed_by": self.signed_by, } class HSMCertificateV2ElementSGXAttestationKey(HSMCertificateV2Element): - def __init__(self, name, message, key, auth_data, signature, signed_by): - self.name = name - self.message = message - self.key = key - self.auth_data = auth_data - self.signature = signature - self.signed_by = signed_by + def __init__(self, element_map): + self._init_with_map(element_map) + + def _init_with_map(self, element_map): + super()._init_with_map(element_map) + + if not is_nonempty_hex_string(element_map.get("message")): + raise ValueError(f"Invalid message for HSM certificate element {self.name}") + self._message = bytes.fromhex(element_map["message"]) + + if not is_nonempty_hex_string(element_map.get("key")): + raise ValueError(f"Invalid key for HSM certificate element {self.name}") + self._key = bytes.fromhex(element_map["key"]) + + if not is_nonempty_hex_string(element_map.get("auth_data")): + raise ValueError(f"Invalid auth data for HSM certificate element {self.name}") + self._auth_data = bytes.fromhex(element_map["auth_data"]) + + if not is_nonempty_hex_string(element_map.get("signature")): + raise ValueError(f"Invalid signature for HSM certificate element {self.name}") + self._signature = bytes.fromhex(element_map["signature"]) + + @property + def message(self): + return self._message.hex() + + @property + def key(self): + return self._key.hex() + + @property + def auth_data(self): + return self._auth_data.hex() + + @property + def signature(self): + return self._signature.hex() def to_dict(self): return { "name": self.name, "type": "sgx_attestation_key", - "message": self.message.hex(), - "key": self.key.hex(), - "auth_data": self.auth_data.hex(), - "signature": self.signature.hex(), + "message": self.message, + "key": self.key, + "auth_data": self.auth_data, + "signature": self.signature, "signed_by": self.signed_by, } class HSMCertificateV2ElementX509(HSMCertificateV2Element): - def __init__(self, name, message, signed_by): - self.name = name - self.message = message - self.signed_by = signed_by + def __init__(self, element_map): + self._init_with_map(element_map) + + def _init_with_map(self, element_map): + super()._init_with_map(element_map) + + try: + self._message = base64.b64decode(element_map.get("message")) + except Exception: + raise ValueError(f"Invalid message for HSM certificate element {self.name}") + + @property + def message(self): + return base64.b64encode(self._message).decode("ASCII") def to_dict(self): return { "name": self.name, "type": "x509_pem", - "message": self.message.decode('ASCII'), + "message": self.message, "signed_by": self.signed_by, } +# Element type mappings +HSMCertificateV2Element.TYPE_MAPPING = { + "sgx_quote": HSMCertificateV2ElementSGXQuote, + "sgx_attestation_key": HSMCertificateV2ElementSGXAttestationKey, + "x509_pem": HSMCertificateV2ElementX509, +} + + class HSMCertificateV2(HSMCertificate): VERSION = 2 + ROOT_ELEMENT = "sgx_root" ELEMENT_BASE_CLASS = HSMCertificateV2Element + ELEMENT_FACTORY = HSMCertificateV2Element.from_dict def validate_and_get_values(self, raw_root_pubkey_hex): # TODO pass - - def _parse(self, certificate_map): - # TODO - pass diff --git a/middleware/admin/sgx_attestation.py b/middleware/admin/sgx_attestation.py index 364df5b2..8267e678 100644 --- a/middleware/admin/sgx_attestation.py +++ b/middleware/admin/sgx_attestation.py @@ -98,34 +98,34 @@ def do_attestation(options): att_cert = HSMCertificateV2() att_cert.add_element( - HSMCertificateV2ElementSGXQuote( - name="quote", - message=envelope.quote.get_raw_data(), - custom_data=envelope.custom_message, - signature=quote_signature, - signed_by="attestation", - )) + HSMCertificateV2ElementSGXQuote({ + "name": "quote", + "message": envelope.quote.get_raw_data().hex(), + "custom_data": envelope.custom_message.hex(), + "signature": quote_signature.hex(), + "signed_by": "attestation", + })) att_cert.add_element( - HSMCertificateV2ElementSGXAttestationKey( - name="attestation", - message=envelope.quote_auth_data.qe_report_body.get_raw_data(), - key=att_key.to_string("uncompressed"), - auth_data=envelope.qe_auth_data.data, - signature=qe_rb_signature, - signed_by="quoting_enclave", - )) + HSMCertificateV2ElementSGXAttestationKey({ + "name": "attestation", + "message": envelope.quote_auth_data.qe_report_body.get_raw_data().hex(), + "key": att_key.to_string("uncompressed").hex(), + "auth_data": envelope.qe_auth_data.data.hex(), + "signature": qe_rb_signature.hex(), + "signed_by": "quoting_enclave", + })) att_cert.add_element( - HSMCertificateV2ElementX509( - name="quoting_enclave", - message=envelope.qe_cert_data.certs[0], - signed_by="platform_ca", - )) + HSMCertificateV2ElementX509({ + "name": "quoting_enclave", + "message": envelope.qe_cert_data.certs[0], + "signed_by": "platform_ca", + })) att_cert.add_element( - HSMCertificateV2ElementX509( - name="platform_ca", - message=envelope.qe_cert_data.certs[1], - signed_by="sgx_root", - )) + HSMCertificateV2ElementX509({ + "name": "platform_ca", + "message": envelope.qe_cert_data.certs[1], + "signed_by": "sgx_root", + })) att_cert.add_target("quote") att_cert.save_to_jsonfile(options.output_file_path) diff --git a/middleware/tests/admin/test_certificate_v1.py b/middleware/tests/admin/test_certificate_v1.py index f7503bba..8235850a 100644 --- a/middleware/tests/admin/test_certificate_v1.py +++ b/middleware/tests/admin/test_certificate_v1.py @@ -26,7 +26,7 @@ from unittest import TestCase from unittest.mock import call, patch, mock_open -from admin.certificate_v1 import HSMCertificate, HSMCertificateElement +from admin.certificate import HSMCertificate, HSMCertificateElement class TestHSMCertificate(TestCase): @@ -155,7 +155,7 @@ def test_create_certificate_missing_elements(self): "targets": ["attestation", "device"] }) - @patch('admin.certificate_v1.HSMCertificateElement') + @patch('admin.certificate_v1.HSMCertificate.ELEMENT_FACTORY') def test_create_certificate_invalid_element(self, certElementMock): certElementMock.side_effect = ValueError() with self.assertRaises(ValueError): diff --git a/middleware/tests/admin/test_certificate_v2.py b/middleware/tests/admin/test_certificate_v2.py index f4a16da6..ada5c485 100644 --- a/middleware/tests/admin/test_certificate_v2.py +++ b/middleware/tests/admin/test_certificate_v2.py @@ -22,9 +22,11 @@ from unittest import TestCase from admin.certificate_v1 import HSMCertificate -from admin.certificate_v2 import HSMCertificateV2, HSMCertificateV2ElementSGXQuote, \ +from admin.certificate_v2 import HSMCertificateV2, HSMCertificateV2Element, \ + HSMCertificateV2ElementSGXQuote, \ HSMCertificateV2ElementSGXAttestationKey, \ HSMCertificateV2ElementX509 +from .test_certificate_v2_resources import TEST_CERTIFICATE class TestHSMCertificateV2(TestCase): @@ -35,16 +37,61 @@ def test_create_empty_certificate_ok(self): cert = HSMCertificateV2() self.assertEqual({'version': 2, 'targets': [], 'elements': []}, cert.to_dict()) + def test_parse_identity(self): + cert = HSMCertificateV2(TEST_CERTIFICATE) + self.assertEqual(TEST_CERTIFICATE, cert.to_dict()) + + +class TestHSMCertificateV2Element(TestCase): + def test_from_dict_unknown_type(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "a-strange-name", + "type": "an-unknown-type", + "some": "other", + "random": "attributes", + }) + self.assertIn("a-strange-name", str(e.exception)) + + def test_from_dict_no_name(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "type": "sgx_quote", + "signed_by": "a-signer", + "some": "other", + "random": "attributes", + }) + self.assertIn("Missing name", str(e.exception)) + + def test_from_dict_no_signed_by(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "a name", + "type": "sgx_quote", + "some": "other", + "random": "attributes", + }) + self.assertIn("Missing certifier", str(e.exception)) + class TestHSMCertificateV2ElementSGXQuote(TestCase): + def setUp(self): + self.elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": "aabbcc", + "custom_data": "ddeeff", + "signature": "112233", + "signed_by": "whosigned", + }) + + def test_props(self): + self.assertEqual("thename", self.elem.name) + self.assertEqual("whosigned", self.elem.signed_by) + self.assertEqual("aabbcc", self.elem.message) + self.assertEqual("ddeeff", self.elem.custom_data) + self.assertEqual("112233", self.elem.signature) + def test_dict_ok(self): - elem = HSMCertificateV2ElementSGXQuote( - "thename", - bytes.fromhex("aabbcc"), - bytes.fromhex("ddeeff"), - bytes.fromhex("112233"), - "whosigned" - ) self.assertEqual({ "name": "thename", "type": "sgx_quote", @@ -52,19 +99,71 @@ def test_dict_ok(self): "custom_data": "ddeeff", "signature": "112233", "signed_by": "whosigned", - }, elem.to_dict()) + }, self.elem.to_dict()) + + def test_parse_identity(self): + source = TEST_CERTIFICATE["elements"][0] + elem = HSMCertificateV2Element.from_dict(source) + self.assertTrue(isinstance(elem, HSMCertificateV2ElementSGXQuote)) + self.assertEqual(source, elem.to_dict()) + + def test_from_dict_invalid_message(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quote", + "type": "sgx_quote", + "message": "not-hex", + "custom_data": "112233", + "signature": "445566778899", + "signed_by": "attestation" + }) + self.assertIn("Invalid message", str(e.exception)) + + def test_from_dict_invalid_custom_data(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quote", + "type": "sgx_quote", + "message": "aabbccdd", + "custom_data": "not-hex", + "signature": "445566778899", + "signed_by": "attestation" + }) + self.assertIn("Invalid custom data", str(e.exception)) + + def test_from_dict_invalid_signature(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quote", + "type": "sgx_quote", + "message": "aabbccdd", + "custom_data": "112233", + "signature": "not-hex", + "signed_by": "attestation" + }) + self.assertIn("Invalid signature", str(e.exception)) class TestHSMCertificateV2ElementSGXAttestationKey(TestCase): + def setUp(self): + self.elem = HSMCertificateV2ElementSGXAttestationKey({ + "name": "thename", + "message": "aabbcc", + "key": "ddeeff", + "auth_data": "112233", + "signature": "44556677", + "signed_by": "whosigned", + }) + + def test_props(self): + self.assertEqual("thename", self.elem.name) + self.assertEqual("whosigned", self.elem.signed_by) + self.assertEqual("aabbcc", self.elem.message) + self.assertEqual("ddeeff", self.elem.key) + self.assertEqual("112233", self.elem.auth_data) + self.assertEqual("44556677", self.elem.signature) + def test_dict_ok(self): - elem = HSMCertificateV2ElementSGXAttestationKey( - "thename", - bytes.fromhex("aabbcc"), - bytes.fromhex("ddeeff"), - bytes.fromhex("112233"), - bytes.fromhex("44556677"), - "whosigned" - ) self.assertEqual({ "name": "thename", "type": "sgx_attestation_key", @@ -73,19 +172,100 @@ def test_dict_ok(self): "auth_data": "112233", "signature": "44556677", "signed_by": "whosigned", - }, elem.to_dict()) + }, self.elem.to_dict()) + + def test_parse_identity(self): + source = TEST_CERTIFICATE["elements"][1] + elem = HSMCertificateV2Element.from_dict(source) + self.assertTrue(isinstance(elem, HSMCertificateV2ElementSGXAttestationKey)) + self.assertEqual(source, elem.to_dict()) + + def test_from_dict_invalid_message(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "attestation", + "type": "sgx_attestation_key", + "message": "not-hex", + "key": "eeff", + "auth_data": "112233", + "signature": "44556677", + "signed_by": "quoting_enclave" + }) + self.assertIn("Invalid message", str(e.exception)) + + def test_from_dict_invalid_key(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "attestation", + "type": "sgx_attestation_key", + "message": "aabbccdd", + "key": "not-hex", + "auth_data": "112233", + "signature": "44556677", + "signed_by": "quoting_enclave" + }) + self.assertIn("Invalid key", str(e.exception)) + + def test_from_dict_invalid_auth_data(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "attestation", + "type": "sgx_attestation_key", + "message": "aabbccdd", + "key": "eeff", + "auth_data": "not-hex", + "signature": "44556677", + "signed_by": "quoting_enclave" + }) + self.assertIn("Invalid auth data", str(e.exception)) + + def test_from_dict_invalid_signature(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "attestation", + "type": "sgx_attestation_key", + "message": "aabbccdd", + "key": "eeff", + "auth_data": "112233", + "signature": "not-hex", + "signed_by": "quoting_enclave" + }) + self.assertIn("Invalid signature", str(e.exception)) class TestHSMCertificateV2ElementX509(TestCase): + def setUp(self): + self.elem = HSMCertificateV2ElementX509({ + "name": "thename", + "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", + "signed_by": "whosigned", + }) + + def test_props(self): + self.assertEqual("thename", self.elem.name) + self.assertEqual("whosigned", self.elem.signed_by) + self.assertEqual("dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", self.elem.message) + def test_dict_ok(self): - elem = HSMCertificateV2ElementX509( - "thename", - b"this is an ascii message", - "whosigned" - ) self.assertEqual({ "name": "thename", "type": "x509_pem", - "message": "this is an ascii message", + "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", "signed_by": "whosigned", - }, elem.to_dict()) + }, self.elem.to_dict()) + + def test_parse_identity(self): + source = TEST_CERTIFICATE["elements"][3] + elem = HSMCertificateV2Element.from_dict(source) + self.assertTrue(isinstance(elem, HSMCertificateV2ElementX509)) + self.assertEqual(source, elem.to_dict()) + + def test_from_dict_invalid_message(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quoting_enclave", + "type": "x509_pem", + "message": "not-base-64", + "signed_by": "platform_ca" + }) + self.assertIn("Invalid message", str(e.exception)) diff --git a/middleware/tests/admin/test_certificate_v2_resources.py b/middleware/tests/admin/test_certificate_v2_resources.py new file mode 100644 index 00000000..4717248a --- /dev/null +++ b/middleware/tests/admin/test_certificate_v2_resources.py @@ -0,0 +1,63 @@ +# 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 json + +TEST_CERTIFICATE = json.loads(""" +{ + "version": 2, + "targets": [ + "quote" + ], + "elements": [ + { + "name": "quote", + "type": "sgx_quote", + "message": "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34d562f4564fc182000000000e0e100fffff01000000000000000000010000000000000000000000000000000000000000000000000000000000000005000000000000000700000000000000d32688d3c1f3dfcc8b0b36eac7c89d49af331800bd56248044166fa6699442c10000000000000000000000000000000000000000000000000000000000000000718c2f1a0efbd513e016fafd6cf62a624442f2d83708d4b33ab5a8d8c1cd4dd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009e95bb875c1a728071f70ad8c9d03f1744c19acb0580921e611ac9104f7701d00000000000000000000000000000000000000000000000000000000000000000", + "custom_data": "504f5748534d3a352e343a3a736778f36f7bc09aab50c0886a442b2d04b18186720bda7a753643066cd0bc0a4191800c4d091913d39750dc8975adbdd261bd10c1c2e110faa47cfbe30e740895552bbdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea4209b00000000000000000000000000000000", + "signature": "3046022100e52b03a7bd6b5dd9feeeb375bd597730d2872643b47aff4dd641c5c3a2b8016e022100bbd227f67e7c23bbddeb4f8fddee031a2b961501d1c28dda082669d7ac861e6c", + "signed_by": "attestation" + }, + { + "name": "attestation", + "type": "sgx_attestation_key", + "message": "0e0e100fffff0100000000000000000000000000000000000000000000000000000000000000000000000000000000001500000000000000e70000000000000096b347a64e5a045e27369c26e6dcda51fd7c850e9b3a3a79e718f43261dee1e400000000000000000000000000000000000000000000000000000000000000008c4f5775d796503e96137f77c68a829a0056ac8ded70140b081b094490c57bff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001fe721d0322954821589237fd27efb8fef1acb3ecd6b0352c31271550fc70f940000000000000000000000000000000000000000000000000000000000000000", + "key": "04a024cb34c90ea6a8f9f2181c9020cbcc7c073e69981733c8deed6f6c451822aa08376350ff7da01f842bb40c631cbb711f8b6f7a4fae398320a3884774d250ad", + "auth_data": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "signature": "304502201f14d532274c4385fc0019ca2a21e53e17143cb62377ca4fcdd97fa9fef8fb2502210095d4ee272cf3c512e36779de67dc7814982f1160d981d138a32b265e928a0562", + "signed_by": "quoting_enclave" + }, + { + "name": "quoting_enclave", + "type": "x509_pem", + "message": "MIIE8zCCBJigAwIBAgIUfr2dlwN42DBUA9CXIkBlGP2vV3AwCgYIKoZIzj0EAwIwcDEiMCAGA1UEAwwZSW50ZWwgU0dYIFBDSyBQbGF0Zm9ybSBDQTEaMBgGA1UECgwRSW50ZWwgQ29ycG9yYXRpb24xFDASBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTELMAkGA1UEBhMCVVMwHhcNMjQwMzIzMDQ0NjIxWhcNMzEwMzIzMDQ0NjIxWjBwMSIwIAYDVQQDDBlJbnRlbCBTR1ggUENLIENlcnRpZmljYXRlMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKl7RDNlsZKkEtAcW7SfCX1JegbvGq4O0rRUt0z/G6fZJsNlpmRwTB4DYkrgkm1t+9RpLwxFX9/kghxiDQm0jqmjggMOMIIDCjAfBgNVHSMEGDAWgBSVb13NvRvh6UBJydT0M84BVwveVDBrBgNVHR8EZDBiMGCgXqBchlpodHRwczovL2FwaS50cnVzdGVkc2VydmljZXMuaW50ZWwuY29tL3NneC9jZXJ0aWZpY2F0aW9uL3Y0L3Bja2NybD9jYT1wbGF0Zm9ybSZlbmNvZGluZz1kZXIwHQYDVR0OBBYEFALKV5DF16KnEbSW5QM9ecDqBZaHMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMIICOwYJKoZIhvhNAQ0BBIICLDCCAigwHgYKKoZIhvhNAQ0BAQQQttJXuiQVwqM4s74g+HxfKTCCAWUGCiqGSIb4TQENAQIwggFVMBAGCyqGSIb4TQENAQIBAgEOMBAGCyqGSIb4TQENAQICAgEOMBAGCyqGSIb4TQENAQIDAgEDMBAGCyqGSIb4TQENAQIEAgEDMBEGCyqGSIb4TQENAQIFAgIA/zARBgsqhkiG+E0BDQECBgICAP8wEAYLKoZIhvhNAQ0BAgcCAQEwEAYLKoZIhvhNAQ0BAggCAQAwEAYLKoZIhvhNAQ0BAgkCAQAwEAYLKoZIhvhNAQ0BAgoCAQAwEAYLKoZIhvhNAQ0BAgsCAQAwEAYLKoZIhvhNAQ0BAgwCAQAwEAYLKoZIhvhNAQ0BAg0CAQAwEAYLKoZIhvhNAQ0BAg4CAQAwEAYLKoZIhvhNAQ0BAg8CAQAwEAYLKoZIhvhNAQ0BAhACAQAwEAYLKoZIhvhNAQ0BAhECAQ0wHwYLKoZIhvhNAQ0BAhIEEA4OAwP//wEAAAAAAAAAAAAwEAYKKoZIhvhNAQ0BAwQCAAAwFAYKKoZIhvhNAQ0BBAQGAGBqAAAAMA8GCiqGSIb4TQENAQUKAQEwHgYKKoZIhvhNAQ0BBgQQDVe/DXUVE4gemtgO5uBpvDBEBgoqhkiG+E0BDQEHMDYwEAYLKoZIhvhNAQ0BBwEBAf8wEAYLKoZIhvhNAQ0BBwIBAQAwEAYLKoZIhvhNAQ0BBwMBAQAwCgYIKoZIzj0EAwIDSQAwRgIhAJFgf78HggTBtvQPXZJx/3Fm71vCOmt82pce91M2ZAI0AiEAiZMPBbZZmvR2v+1mrs76JeglDQ+pK/SLN94l4+jM5DA=", + "signed_by": "platform_ca" + }, + { + "name": "platform_ca", + "type": "x509_pem", + "message": "MIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMCMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAgBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJleTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9dzb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1UenA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+", + "signed_by": "sgx_root" + } + ] +} +""") diff --git a/middleware/tests/admin/test_sgx_attestation.py b/middleware/tests/admin/test_sgx_attestation.py index 4b60fadb..78d1e8de 100644 --- a/middleware/tests/admin/test_sgx_attestation.py +++ b/middleware/tests/admin/test_sgx_attestation.py @@ -65,7 +65,7 @@ def setupMocks(self, get_hsm, get_ud_value_for_attestation, do_unlock, "envelope": "11"*32, "message": "22"*32, } - quote = SimpleNamespace(**{"get_raw_data": lambda: "quote-raw-data"}) + quote = SimpleNamespace(**{"get_raw_data": lambda: b"quote-raw-data"}) sig = SimpleNamespace(**{"r": b"a"*32, "s": b"a"*32}) self.att_key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p).\ get_verifying_key() @@ -76,11 +76,11 @@ def setupMocks(self, get_hsm, get_ud_value_for_attestation, do_unlock, "signature": sig, "attestation_key": attkey, "qe_report_body": SimpleNamespace(**{ - "get_raw_data": lambda: "qerb-raw-data"}), + "get_raw_data": lambda: b"qerb-raw-data"}), "qe_report_body_signature": qesig, }) qead = SimpleNamespace(**{ - "data": "qead-data", + "data": b"qead-data", }) qecd = SimpleNamespace(**{ "certs": ["qecd-cert-0", "qecd-cert-1"], @@ -90,7 +90,7 @@ def setupMocks(self, get_hsm, get_ud_value_for_attestation, do_unlock, "quote_auth_data": qad, "qe_auth_data": qead, "qe_cert_data": qecd, - "custom_message": "a-custom-message", + "custom_message": b"a-custom-message", }) self.SgxEnvelope.return_value = envelope @@ -123,32 +123,32 @@ def test_ok(self, *args): bytes.fromhex("11"*32), bytes.fromhex("22"*32), ) - self.HSMCertificateV2ElementSGXQuote.assert_called_with( - name="quote", - message="quote-raw-data", - custom_data="a-custom-message", - signature=bytes.fromhex("30440220"+"61"*32+"0220"+"61"*32), - signed_by="attestation", - ) - self.HSMCertificateV2ElementSGXAttestationKey.assert_called_with( - name="attestation", - message="qerb-raw-data", - key=self.att_key.to_string("uncompressed"), - auth_data="qead-data", - signature=bytes.fromhex("30440220"+"63"*32+"0220"+"63"*32), - signed_by="quoting_enclave", - ) + self.HSMCertificateV2ElementSGXQuote.assert_called_with({ + "name": "quote", + "message": b"quote-raw-data".hex(), + "custom_data": b"a-custom-message".hex(), + "signature": "30440220"+"61"*32+"0220"+"61"*32, + "signed_by": "attestation", + }) + self.HSMCertificateV2ElementSGXAttestationKey.assert_called_with({ + "name": "attestation", + "message": b"qerb-raw-data".hex(), + "key": self.att_key.to_string("uncompressed").hex(), + "auth_data": b"qead-data".hex(), + "signature": "30440220"+"63"*32+"0220"+"63"*32, + "signed_by": "quoting_enclave", + }) self.HSMCertificateV2ElementX509.assert_has_calls([ - call( - name="quoting_enclave", - message="qecd-cert-0", - signed_by="platform_ca", - ), - call( - name="platform_ca", - message="qecd-cert-1", - signed_by="sgx_root", - ) + call({ + "name": "quoting_enclave", + "message": "qecd-cert-0", + "signed_by": "platform_ca", + }), + call({ + "name": "platform_ca", + "message": "qecd-cert-1", + "signed_by": "sgx_root", + }) ]) cert = self.HSMCertificateV2.return_value cert.add_element.assert_has_calls([ diff --git a/setup.cfg b/setup.cfg index da90c29b..36a420ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ per-file-ignores = __init__.py:F401, middleware/admin/certificate.py:F401, middleware/tests/sgx/test_envelope.py:E122, + middleware/tests/admin/test_certificate_v2_resources.py:E501, show-source = False statistics = True From 6f54ca3c0a851362fa5b425809131732cc3deed9 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Tue, 24 Dec 2024 00:30:50 +1300 Subject: [PATCH 11/21] Certificate v2 validator (#231) - Replacing plain public key in validate_and_get_values method for a root of trust object with get_pubkey method - Using inherited validate_and_get_values in HSMCertificateV2 - Added HSMCertificateRoot to certificate version 1 to act as root of trust in existing validations - Updated verify ledger attestation accordingly - Added and updated unit tests --- middleware/admin/certificate.py | 2 +- middleware/admin/certificate_v1.py | 42 +++++++++++------ middleware/admin/certificate_v2.py | 26 ++++++++--- middleware/admin/verify_ledger_attestation.py | 6 ++- middleware/tests/admin/test_certificate_v1.py | 45 +++---------------- .../admin/test_certificate_v1_element.py | 22 +++++++-- middleware/tests/admin/test_certificate_v2.py | 12 +++++ 7 files changed, 90 insertions(+), 65 deletions(-) diff --git a/middleware/admin/certificate.py b/middleware/admin/certificate.py index 1321a0b2..ad14cbd6 100644 --- a/middleware/admin/certificate.py +++ b/middleware/admin/certificate.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from .certificate_v1 import HSMCertificate, HSMCertificateElement +from .certificate_v1 import HSMCertificate, HSMCertificateRoot, HSMCertificateElement from .certificate_v2 import HSMCertificateV2, HSMCertificateV2ElementSGXQuote, \ HSMCertificateV2ElementSGXAttestationKey, \ HSMCertificateV2ElementX509 diff --git a/middleware/admin/certificate_v1.py b/middleware/admin/certificate_v1.py index 414fe8f3..842793a0 100644 --- a/middleware/admin/certificate_v1.py +++ b/middleware/admin/certificate_v1.py @@ -27,6 +27,21 @@ from .utils import is_nonempty_hex_string +class HSMCertificateRoot: + def __init__(self, raw_pubkey_hex): + # Parse the public key + try: + self.pubkey = ec.PublicKey(bytes.fromhex(raw_pubkey_hex), raw=True) + except Exception: + raise ValueError("Error parsing certificate root public key") + + def __repr__(self): + return self.pubkey.serialize(compressed=False).hex() + + def get_pubkey(self): + return self.pubkey + + class HSMCertificateElement: VALID_NAMES = ["device", "attestation", "ui", "signer"] EXTRACTORS = { @@ -98,10 +113,11 @@ def to_dict(self): return result - def is_valid(self, certifier_pubkey): + def is_valid(self, certifier): try: message = bytes.fromhex(self.message) + certifier_pubkey = certifier.get_pubkey() verifier_pubkey = certifier_pubkey if self.tweak is not None: tweak = hmac.new( @@ -120,6 +136,12 @@ def is_valid(self, certifier_pubkey): def get_value(self): return self.EXTRACTORS[self.name](bytes.fromhex(self.message)).hex() + def get_pubkey(self): + return ec.PublicKey(bytes.fromhex(self.get_value()), raw=True) + + def get_tweak(self): + return self.tweak + class HSMCertificate: VERSION = 1 # Only supported version @@ -155,14 +177,7 @@ def __init__(self, certificate_map=None): if certificate_map is not None: self._parse(certificate_map) - def validate_and_get_values(self, raw_root_pubkey_hex): - # Parse the root public key - try: - root_pubkey = ec.PublicKey(bytes.fromhex(raw_root_pubkey_hex), raw=True) - except Exception: - return dict([(target, (False, self.ROOT_ELEMENT)) - for target in self._targets]) - + def validate_and_get_values(self, root_of_trust): result = {} for target in self._targets: # Build the chain from the target to the root @@ -178,19 +193,18 @@ def validate_and_get_values(self, raw_root_pubkey_hex): # If valid, return True and the value of the leaf # If not valid, return False and the name of the element that # failed the validation - current_pubkey = root_pubkey + current_certifier = root_of_trust while True: # Validate this element - if not current.is_valid(current_pubkey): + if not current.is_valid(current_certifier): result[target] = (False, current.name) break # Reached the leaf? => valid! if len(chain) == 0: - result[target] = (True, current.get_value(), current.tweak) + result[target] = (True, current.get_value(), current.get_tweak()) break - current_pubkey = ec.PublicKey(bytes.fromhex(current.get_value()), - raw=True) + current_certifier = current current = chain.pop() return result diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index a7b58f7c..ca343025 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -27,8 +27,7 @@ class HSMCertificateV2Element: def __init__(self): - raise RuntimeError("Cannot instantiate an " - "abstract HSMCertificateV2Element") + raise NotImplementedError("Cannot instantiate a HSMCertificateV2Element") @classmethod def from_dict(kls, element_map): @@ -56,6 +55,22 @@ def name(self): def signed_by(self): return self._signed_by + def get_value(self): + raise NotImplementedError(f"{type(self).__name__} can't provide a value") + + def get_pubkey(self): + # TODO: this should yield not implemented + # TODO: implementation should be down to each specific subclass + return None + + def is_valid(self, certifier): + # TODO: this should yield not implemented + # TODO: implementation should be down to each specific subclass + return True + + def get_tweak(self): + return None + class HSMCertificateV2ElementSGXQuote(HSMCertificateV2Element): def __init__(self, element_map): @@ -89,6 +104,9 @@ def custom_data(self): def signature(self): return self._signature.hex() + def get_value(self): + return self.custom_data + def to_dict(self): return { "name": self.name, @@ -189,7 +207,3 @@ class HSMCertificateV2(HSMCertificate): ROOT_ELEMENT = "sgx_root" ELEMENT_BASE_CLASS = HSMCertificateV2Element ELEMENT_FACTORY = HSMCertificateV2Element.from_dict - - def validate_and_get_values(self, raw_root_pubkey_hex): - # TODO - pass diff --git a/middleware/admin/verify_ledger_attestation.py b/middleware/admin/verify_ledger_attestation.py index f09f99c2..d9f7ffd0 100644 --- a/middleware/admin/verify_ledger_attestation.py +++ b/middleware/admin/verify_ledger_attestation.py @@ -26,7 +26,7 @@ import re from .misc import info, head, AdminError from .utils import is_nonempty_hex_string -from .certificate import HSMCertificate +from .certificate import HSMCertificate, HSMCertificateRoot UI_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:UI:(5.[0-9])") @@ -69,6 +69,10 @@ def do_verify_attestation(options): if not is_nonempty_hex_string(options.root_authority): raise AdminError("Invalid root authority") root_authority = options.root_authority + try: + root_authority = HSMCertificateRoot(root_authority) + except ValueError: + raise AdminError("Invalid root authority") info(f"Using {root_authority} as root authority") # Load the given public keys and compute diff --git a/middleware/tests/admin/test_certificate_v1.py b/middleware/tests/admin/test_certificate_v1.py index 8235850a..c6ab1ea0 100644 --- a/middleware/tests/admin/test_certificate_v1.py +++ b/middleware/tests/admin/test_certificate_v1.py @@ -26,7 +26,7 @@ from unittest import TestCase from unittest.mock import call, patch, mock_open -from admin.certificate import HSMCertificate, HSMCertificateElement +from admin.certificate import HSMCertificate, HSMCertificateRoot, HSMCertificateElement class TestHSMCertificate(TestCase): @@ -240,6 +240,7 @@ def test_create_certificate_signer_not_in_elements(self): def test_validate_and_get_values_ok(self): root_privkey = ec.PrivateKey() root_pubkey = root_privkey.pubkey.serialize(compressed=False).hex() + root_of_trust = HSMCertificateRoot(root_pubkey) device_privkey = ec.PrivateKey() device_pubkey = device_privkey.pubkey.serialize(compressed=False).hex() att_pubkey = ec.PrivateKey().pubkey.serialize(compressed=False).hex() @@ -273,48 +274,12 @@ def test_validate_and_get_values_ok(self): self.assertEqual({ 'attestation': (True, att_pubkey, None), 'device': (True, device_pubkey, None) - }, cert.validate_and_get_values(root_pubkey)) - - def test_create_and_get_values_invalid_pubkey(self): - root_privkey = ec.PrivateKey() - device_privkey = ec.PrivateKey() - device_pubkey = device_privkey.pubkey.serialize(compressed=False).hex() - att_pubkey = ec.PrivateKey().pubkey.serialize(compressed=False).hex() - - att_msg = 'ff' + att_pubkey - att_sig = device_privkey.ecdsa_serialize( - device_privkey.ecdsa_sign(bytes.fromhex(att_msg))).hex() - - device_msg = os.urandom(16).hex() + device_pubkey - device_sig = root_privkey.ecdsa_serialize( - root_privkey.ecdsa_sign(bytes.fromhex(device_msg))).hex() - - cert = HSMCertificate({ - "version": 1, - "targets": ["attestation", "device"], - "elements": [ - { - "name": "attestation", - "message": att_msg, - "signature": att_sig, - "signed_by": "device" - }, - { - "name": "device", - "message": device_msg, - "signature": device_sig, - "signed_by": "root" - }] - }) - - self.assertEqual({ - 'attestation': (False, 'root'), - 'device': (False, 'root') - }, cert.validate_and_get_values('invalid-pubkey')) + }, cert.validate_and_get_values(root_of_trust)) def test_validate_and_get_values_invalid_element(self): root_privkey = ec.PrivateKey() root_pubkey = root_privkey.pubkey.serialize(compressed=False).hex() + root_of_trust = HSMCertificateRoot(root_pubkey) device_privkey = ec.PrivateKey() device_pubkey = device_privkey.pubkey.serialize(compressed=False).hex() att_pubkey = ec.PrivateKey().pubkey.serialize(compressed=False).hex() @@ -347,7 +312,7 @@ def test_validate_and_get_values_invalid_element(self): self.assertEqual({ 'attestation': (False, 'attestation'), 'device': (True, device_pubkey, None) - }, cert.validate_and_get_values(root_pubkey)) + }, cert.validate_and_get_values(root_of_trust)) def test_validate_and_get_values_invalid_elements(self): att_privkey = ec.PrivateKey() diff --git a/middleware/tests/admin/test_certificate_v1_element.py b/middleware/tests/admin/test_certificate_v1_element.py index ff2f206c..716f0dfb 100644 --- a/middleware/tests/admin/test_certificate_v1_element.py +++ b/middleware/tests/admin/test_certificate_v1_element.py @@ -27,7 +27,21 @@ import secp256k1 as ec from unittest import TestCase -from admin.certificate import HSMCertificateElement +from unittest.mock import Mock +from admin.certificate import HSMCertificateRoot, HSMCertificateElement + + +class TestHSMCertificateRoot(TestCase): + def test_ok(self): + pubkey = ec.PrivateKey().pubkey + root = HSMCertificateRoot(pubkey.serialize(compressed=False).hex()) + self.assertEqual( + pubkey.serialize(compressed=True), + root.get_pubkey().serialize(compressed=True)) + + def test_invalid_pubkey(self): + with self.assertRaises(ValueError): + HSMCertificateRoot("invalid-pubkey") class TestHSMCertificateElement(TestCase): @@ -100,6 +114,7 @@ def test_certificate_element_is_valid_ok(self): privkey = ec.PrivateKey() msg = 'aa' * 65 signature = privkey.ecdsa_serialize(privkey.ecdsa_sign(bytes.fromhex(msg))).hex() + mock_certifier = Mock(get_pubkey=lambda: privkey.pubkey) element = HSMCertificateElement({ "name": "device", @@ -113,7 +128,7 @@ def test_certificate_element_is_valid_ok(self): "signature": signature, "signed_by": "root" }, element.to_dict()) - self.assertTrue(element.is_valid(privkey.pubkey)) + self.assertTrue(element.is_valid(mock_certifier)) def test_certificate_element_is_valid_with_tweak_ok(self): privkey = ec.PrivateKey() @@ -124,6 +139,7 @@ def test_certificate_element_is_valid_with_tweak_ok(self): pubkey.serialize(compressed=False), hashlib.sha256, ).digest() + mock_certifier = Mock(get_pubkey=lambda: pubkey) tweak_privkey = ec.PrivateKey(privkey.tweak_add(tweak), raw=True) msg = os.urandom(66).hex() @@ -144,7 +160,7 @@ def test_certificate_element_is_valid_with_tweak_ok(self): "signed_by": "root", "tweak": raw_tweak }, element.to_dict()) - self.assertTrue(element.is_valid(pubkey)) + self.assertTrue(element.is_valid(mock_certifier)) def test_certificate_element_is_valid_wrong_signature(self): privkey = ec.PrivateKey() diff --git a/middleware/tests/admin/test_certificate_v2.py b/middleware/tests/admin/test_certificate_v2.py index ada5c485..ed7c0c8d 100644 --- a/middleware/tests/admin/test_certificate_v2.py +++ b/middleware/tests/admin/test_certificate_v2.py @@ -41,6 +41,18 @@ def test_parse_identity(self): cert = HSMCertificateV2(TEST_CERTIFICATE) self.assertEqual(TEST_CERTIFICATE, cert.to_dict()) + def test_validate_and_get_values_value(self): + cert = HSMCertificateV2(TEST_CERTIFICATE) + self.assertEqual({ + "quote": ( + True, + "504f5748534d3a352e343a3a736778f36f7bc09aab50c0886a442b2d04b18186720bd" + "a7a753643066cd0bc0a4191800c4d091913d39750dc8975adbdd261bd10c1c2e110fa" + "a47cfbe30e740895552bbdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6" + "e7191f67ea4209b00000000000000000000000000000000", + None) + }, cert.validate_and_get_values('a-root-of-trust')) + class TestHSMCertificateV2Element(TestCase): def test_from_dict_unknown_type(self): From 304212707c6bc64b171928a5cf50c50b7b2b4e8f Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Sat, 28 Dec 2024 02:06:33 +1300 Subject: [PATCH 12/21] SGX attestation validation (#232) - Added message parsing capabilities to HSMCertificateV2ElementSGXQuote - Added file certificate loading capabilities to HSMCertificateV2ElementX509 - Factored out common behavior between ledger and SGX attestation verification into helper library functions - Added helper library for assorted attestation utils, including a v5+ attestation message parsing class - Added SGX do_verify_attestation function with its corresponding module - Added verify_attestation operation to adm_sgx tool - Added and updated unit tests - Ignoring long line warnings for test attestation utils resources module --- middleware/adm_sgx.py | 26 +- middleware/admin/attestation_utils.py | 143 ++++++++++ middleware/admin/certificate_v2.py | 23 +- middleware/admin/verify_ledger_attestation.py | 119 +++----- middleware/admin/verify_sgx_attestation.py | 125 ++++++++ middleware/tests/admin/test_adm_sgx.py | 15 +- .../tests/admin/test_attestation_utils.py | 269 ++++++++++++++++++ .../admin/test_attestation_utils_resources.py | 36 +++ middleware/tests/admin/test_certificate_v2.py | 58 +++- .../admin/test_verify_ledger_attestation.py | 177 +++++------- .../admin/test_verify_sgx_attestation.py | 241 ++++++++++++++++ setup.cfg | 1 + 12 files changed, 1034 insertions(+), 199 deletions(-) create mode 100644 middleware/admin/attestation_utils.py create mode 100644 middleware/admin/verify_sgx_attestation.py create mode 100644 middleware/tests/admin/test_attestation_utils.py create mode 100644 middleware/tests/admin/test_attestation_utils_resources.py create mode 100644 middleware/tests/admin/test_verify_sgx_attestation.py diff --git a/middleware/adm_sgx.py b/middleware/adm_sgx.py index d6a8f289..7090c5c8 100644 --- a/middleware/adm_sgx.py +++ b/middleware/adm_sgx.py @@ -31,6 +31,7 @@ from admin.pubkeys import do_get_pubkeys from admin.changepin import do_changepin from admin.sgx_attestation import do_attestation +from admin.verify_sgx_attestation import do_verify_attestation def main(): @@ -42,12 +43,13 @@ def main(): "pubkeys": do_get_pubkeys, "changepin": do_changepin, "attestation": do_attestation, + "verify_attestation": do_verify_attestation, } parser = ArgumentParser(description="SGX powHSM Administrative tool") parser.add_argument("operation", choices=list(actions.keys())) parser.add_argument( - "-r", + "-p", "--port", dest="sgx_port", help="SGX powHSM listening port (default 7777)", @@ -61,7 +63,7 @@ def main(): help="SGX powHSM host. (default 'localhost')", default="localhost", ) - parser.add_argument("-p", "--pin", dest="pin", help="PIN.") + parser.add_argument("-P", "--pin", dest="pin", help="PIN.") parser.add_argument( "-n", "--newpin", @@ -103,6 +105,26 @@ def main(): f"{DEFAULT_ATT_UD_SOURCE}). Can also specify a 32-byte hex string to use as" " the value.", ) + parser.add_argument( + "-t", + "--attcert", + dest="attestation_certificate_file_path", + help="Attestation key certificate file (only valid for " + "'verify_attestation' operation).", + ) + parser.add_argument( + "-r", + "--root", + dest="root_authority", + help="Root attestation authority (only valid for 'verify_attestation' " + "operation). Defaults to Intel SGX's root authority.", + ) + parser.add_argument( + "-b", + "--pubkeys", + dest="pubkeys_file_path", + help="Public keys file (only valid for 'verify_attestation' operation).", + ) parser.add_argument( "-v", "--verbose", diff --git a/middleware/admin/attestation_utils.py b/middleware/admin/attestation_utils.py new file mode 100644 index 00000000..d9d31387 --- /dev/null +++ b/middleware/admin/attestation_utils.py @@ -0,0 +1,143 @@ +# 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 hashlib +import json +import re +import secp256k1 as ec +import requests +from pathlib import Path +from comm.cstruct import CStruct +from .misc import AdminError +from .certificate_v2 import HSMCertificateV2, HSMCertificateV2ElementX509 + + +class PowHsmAttestationMessage(CStruct): + """ + pow_hsm_message_header + + uint8_t platform 3 + uint8_t ud_value 32 + uint8_t public_keys_hash 32 + uint8_t best_block 32 + uint8_t last_signed_tx 8 + uint8_t timestamp 8 + """ + + HEADER_REGEX = re.compile(b"^POWHSM:(5.[0-9])::") + + @classmethod + def is_header(kls, value): + return kls.HEADER_REGEX.match(value) is not None + + def __init__(self, value, offset=0, little=True, name="powHSM"): + self.name = name + # Parse header + match = self.HEADER_REGEX.match(value) + if match is None: + raise ValueError( + f"Invalid {self.name} attestation message header: {value.hex()}") + + # Validate total length + header_length = len(match.group(0)) + expected_length = header_length + self.get_bytelength() + if len(value[offset:]) != expected_length: + raise ValueError(f"{self.name} attestation message length " + f"mismatch: {value[offset:].hex()}") + + # Grab version + self.version = match.group(1).decode("ASCII") + + # Parse the rest + super().__init__(value, offset+header_length, little) + + # Conversions + self.platform = self.platform.decode("ASCII") + self.timestamp = int.from_bytes(self.timestamp, byteorder="big", signed=False) + + +def load_pubkeys(pubkeys_file_path): + # Load the given public keys file into a map + try: + with open(pubkeys_file_path, "r") as file: + pubkeys_map = json.loads(file.read()) + + if type(pubkeys_map) != dict: + raise AdminError( + "Public keys file must contain an object as a top level element") + + result = {} + for path in pubkeys_map.keys(): + pubkey = pubkeys_map[path] + try: + pubkey = ec.PublicKey(bytes.fromhex(pubkey), raw=True) + except Exception: + raise AdminError(f"Invalid public key for path {path}: {pubkey}") + result[path] = pubkey + return result + except (FileNotFoundError, ValueError, json.JSONDecodeError) as e: + raise AdminError('Unable to read public keys from "%s": %s' % + (pubkeys_file_path, str(e))) + + +def compute_pubkeys_hash(pubkeys_map): + # Compute the given public keys hash + # (sha256sum of the uncompressed public keys in + # lexicographical path order) + if len(pubkeys_map) == 0: + raise AdminError("Can't compute the hash of an empty public keys map") + + pubkeys_hash = hashlib.sha256() + for path in sorted(pubkeys_map.keys()): + pubkey = pubkeys_map[path] + pubkeys_hash.update(pubkey.serialize(compressed=False)) + return pubkeys_hash.digest() + + +def compute_pubkeys_output(pubkeys_map): + pubkeys_output = [] + path_name_padding = max(map(len, pubkeys_map.keys())) + for path in sorted(pubkeys_map.keys()): + pubkey = pubkeys_map[path] + pubkeys_output.append( + f"{(path + ':').ljust(path_name_padding+1)} " + f"{pubkey.serialize(compressed=True).hex()}" + ) + return pubkeys_output + + +def get_root_of_trust(path): + # From file + if Path(path).is_file(): + return HSMCertificateV2ElementX509.from_pemfile( + path, + HSMCertificateV2.ROOT_ELEMENT, + HSMCertificateV2.ROOT_ELEMENT) + + # Assume URL and try to grab it + ra_res = requests.get(path) + if ra_res.status_code != 200: + raise RuntimeError(f"Error fetching root of trust from {path}") + return HSMCertificateV2ElementX509.from_pem( + ra_res.content.decode(), + HSMCertificateV2.ROOT_ELEMENT, + HSMCertificateV2.ROOT_ELEMENT) diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index ca343025..e84591d2 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -20,9 +20,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import re +from pathlib import Path import base64 from .certificate_v1 import HSMCertificate from .utils import is_nonempty_hex_string +from sgx.envelope import SgxQuote class HSMCertificateV2Element: @@ -105,7 +108,10 @@ def signature(self): return self._signature.hex() def get_value(self): - return self.custom_data + return { + "sgx_quote": SgxQuote(self._message), + "message": self.custom_data, + } def to_dict(self): return { @@ -170,6 +176,21 @@ def to_dict(self): class HSMCertificateV2ElementX509(HSMCertificateV2Element): + @classmethod + def from_pemfile(kls, pem_path, name, signed_by): + return kls.from_pem(Path(pem_path).read_text(), name, signed_by) + + @classmethod + def from_pem(kls, pem_str, name, signed_by): + return kls({ + "name": name, + "message": re.sub(r"[\s\n\r]+", " ", pem_str) + .replace("-----END CERTIFICATE-----", "") + .replace("-----BEGIN CERTIFICATE-----", "") + .strip().encode(), + "signed_by": signed_by, + }) + def __init__(self, element_map): self._init_with_map(element_map) diff --git a/middleware/admin/verify_ledger_attestation.py b/middleware/admin/verify_ledger_attestation.py index d9f7ffd0..475a1ef5 100644 --- a/middleware/admin/verify_ledger_attestation.py +++ b/middleware/admin/verify_ledger_attestation.py @@ -20,32 +20,23 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import json -import hashlib -import secp256k1 as ec import re from .misc import info, head, AdminError +from .attestation_utils import PowHsmAttestationMessage, load_pubkeys, \ + compute_pubkeys_hash, compute_pubkeys_output from .utils import is_nonempty_hex_string from .certificate import HSMCertificate, HSMCertificateRoot -UI_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:UI:(5.[0-9])") -SIGNER_LEGACY_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:SIGNER:(5.[0-9])") +UI_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:UI:([2,3,4,5].[0-9])") +SIGNER_LEGACY_MESSAGE_HEADER_REGEX = re.compile(b"^HSM:SIGNER:([2,3,4,5].[0-9])") UI_DERIVATION_PATH = "m/44'/0'/0'/0/0" UD_VALUE_LENGTH = 32 +PUBLIC_KEYS_HASH_LENGTH = 32 PUBKEY_COMPRESSED_LENGTH = 33 SIGNER_HASH_LENGTH = 32 SIGNER_ITERATION_LENGTH = 2 -# New signer message header with fields -SIGNER_MESSAGE_HEADER_REGEX = re.compile(b"^POWHSM:(5.[0-9])::") -SM_PLATFORM_LEN = 3 -SM_UD_LEN = 32 -SM_PKH_LEN = 32 -SM_BB_LEN = 32 -SM_TXN_LEN = 8 -SM_TMSTMP_LEN = 8 - # Ledger's root authority # (according to # https://github.com/LedgerHQ/blue-loader-python/blob/master/ledgerblue/ @@ -75,45 +66,18 @@ def do_verify_attestation(options): raise AdminError("Invalid root authority") info(f"Using {root_authority} as root authority") - # Load the given public keys and compute - # their hash (sha256sum of the uncompressed - # public keys in lexicographical path order) - # Also find and save the public key corresponding - # to the expected derivation path for the UI - # attestation - expected_ui_public_key = None - try: - with open(options.pubkeys_file_path, "r") as file: - pubkeys_map = json.loads(file.read()) - - if type(pubkeys_map) != dict: - raise ValueError( - "Public keys file must contain an object as a top level element") - - pubkeys_hash = hashlib.sha256() - pubkeys_output = [] - path_name_padding = max(map(len, pubkeys_map.keys())) - for path in sorted(pubkeys_map.keys()): - pubkey = pubkeys_map[path] - if not is_nonempty_hex_string(pubkey): - raise AdminError(f"Invalid public key for path {path}: {pubkey}") - pubkey = ec.PublicKey(bytes.fromhex(pubkey), raw=True) - pubkeys_hash.update(pubkey.serialize(compressed=False)) - pubkeys_output.append( - f"{(path + ':').ljust(path_name_padding+1)} " - f"{pubkey.serialize(compressed=True).hex()}" - ) - if path == UI_DERIVATION_PATH: - expected_ui_public_key = pubkey.serialize(compressed=True).hex() - pubkeys_hash = pubkeys_hash.digest() - - except (ValueError, json.JSONDecodeError) as e: - raise ValueError('Unable to read public keys from "%s": %s' % - (options.pubkeys_file_path, str(e))) + # Load public keys, compute their hash and format them for output + pubkeys_map = load_pubkeys(options.pubkeys_file_path) + pubkeys_hash = compute_pubkeys_hash(pubkeys_map) + pubkeys_output = compute_pubkeys_output(pubkeys_map) + # Find the expected UI public key + expected_ui_public_key = next(filter( + lambda pair: pair[0] == UI_DERIVATION_PATH, pubkeys_map.items()), (None, None))[1] if expected_ui_public_key is None: raise AdminError( f"Public key with path {UI_DERIVATION_PATH} not present in public key file") + expected_ui_public_key = expected_ui_public_key.serialize(compressed=True).hex() # Load the given attestation key certificate try: @@ -155,6 +119,10 @@ def do_verify_attestation(options): SIGNER_HASH_LENGTH + SIGNER_ITERATION_LENGTH] signer_iteration = int.from_bytes(signer_iteration, byteorder='big', signed=False) + if ui_public_key != expected_ui_public_key: + raise AdminError("Invalid UI attestation: unexpected public key reported. " + f"Expected {expected_ui_public_key} but got {ui_public_key}") + head( [ "UI verified with:", @@ -180,40 +148,31 @@ def do_verify_attestation(options): signer_message = bytes.fromhex(signer_result[1]) signer_hash = bytes.fromhex(signer_result[2]) lmh_match = SIGNER_LEGACY_MESSAGE_HEADER_REGEX.match(signer_message) - mh_match = SIGNER_MESSAGE_HEADER_REGEX.match(signer_message) - if lmh_match is None and mh_match is None: + if lmh_match is None and not PowHsmAttestationMessage.is_header(signer_message): raise AdminError( f"Invalid Signer attestation message header: {signer_message.hex()}") if lmh_match is not None: # Legacy header + powhsm_message = None hlen = len(lmh_match.group(0)) - signer_version = lmh_match.group(1) + signer_version = lmh_match.group(1).decode() offset = hlen reported_pubkeys_hash = signer_message[offset:] - offset += SM_PKH_LEN + offset += PUBLIC_KEYS_HASH_LENGTH + if signer_message[offset:] != b'': + raise AdminError(f"Signer attestation message longer " + f"than expected: {signer_message.hex()}") else: # New header - hlen = len(mh_match.group(0)) - signer_version = mh_match.group(1) - offset = hlen - reported_platform = signer_message[offset:offset+SM_PLATFORM_LEN] - offset += SM_PLATFORM_LEN - reported_ud_value = signer_message[offset:offset+SM_UD_LEN] - offset += SM_UD_LEN - reported_pubkeys_hash = signer_message[offset:offset+SM_PKH_LEN] - offset += SM_PKH_LEN - reported_best_block = signer_message[offset:offset+SM_BB_LEN] - offset += SM_BB_LEN - reported_txn_head = signer_message[offset:offset+SM_TXN_LEN] - offset += SM_TXN_LEN - reported_timestamp = signer_message[offset:offset+SM_TMSTMP_LEN] - offset += SM_TMSTMP_LEN - - if signer_message[offset:] != b'': - raise AdminError(f"Signer attestation message longer " - f"than expected: {signer_message.hex()}") - + try: + powhsm_message = PowHsmAttestationMessage(signer_message, name="Signer") + except ValueError as e: + raise AdminError(str(e)) + signer_version = powhsm_message.version + reported_pubkeys_hash = powhsm_message.public_keys_hash + + # Validations on extracted values if reported_pubkeys_hash != pubkeys_hash: raise AdminError( f"Signer attestation public keys hash mismatch: expected {pubkeys_hash.hex()}" @@ -224,16 +183,16 @@ def do_verify_attestation(options): f"Hash: {pubkeys_hash.hex()}", "", f"Installed Signer hash: {signer_hash.hex()}", - f"Installed Signer version: {signer_version.decode()}", + f"Installed Signer version: {signer_version}", ] - if mh_match is not None: + if powhsm_message is not None: signer_info += [ - f"Platform: {reported_platform.decode("ASCII")}", - f"UD value: {reported_ud_value.hex()}", - f"Best block: {reported_best_block.hex()}", - f"Last transaction signed: {reported_txn_head.hex()}", - f"Timestamp: {reported_timestamp.hex()}", + f"Platform: {powhsm_message.platform}", + f"UD value: {powhsm_message.ud_value.hex()}", + f"Best block: {powhsm_message.best_block.hex()}", + f"Last transaction signed: {powhsm_message.last_signed_tx.hex()}", + f"Timestamp: {powhsm_message.timestamp}", ] head( diff --git a/middleware/admin/verify_sgx_attestation.py b/middleware/admin/verify_sgx_attestation.py new file mode 100644 index 00000000..1e8672fb --- /dev/null +++ b/middleware/admin/verify_sgx_attestation.py @@ -0,0 +1,125 @@ +# 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. + +from .misc import info, head, AdminError +from .attestation_utils import PowHsmAttestationMessage, load_pubkeys, \ + compute_pubkeys_hash, compute_pubkeys_output, \ + get_root_of_trust +from .certificate import HSMCertificate + + +# ################################################################################### +# As default root authority, we use the Provisioning Certification Root CA from Intel +# The Provisioning Certification Root CA is available for download +# from Intel, as described here: +# https://api.portal.trustedservices.intel.com/content/documentation.html + +DEFAULT_ROOT_AUTHORITY = "https://certificates.trustedservices.intel.com/"\ + "Intel_SGX_Provisioning_Certification_RootCA.pem" + +# ################################################################################### + + +def do_verify_attestation(options): + head("### -> Verify powHSM attestation", fill="#") + + if options.attestation_certificate_file_path is None: + raise AdminError("No attestation certificate file given") + + if options.pubkeys_file_path is None: + raise AdminError("No public keys file given") + + # Load root authority + root_authority = options.root_authority or DEFAULT_ROOT_AUTHORITY + info(f"Attempting to gather root authority from {root_authority}...") + try: + root_of_trust = get_root_of_trust(root_authority) + except Exception as e: + raise AdminError(f"Invalid root authority {root_authority}: {e}") + info(f"Using {root_authority} as root authority") + + # Load public keys, compute their hash and format them for output + try: + pubkeys_map = load_pubkeys(options.pubkeys_file_path) + pubkeys_hash = compute_pubkeys_hash(pubkeys_map) + pubkeys_output = compute_pubkeys_output(pubkeys_map) + except Exception as e: + raise AdminError(str(e)) + + # Load the given attestation key certificate + try: + att_cert = HSMCertificate.from_jsonfile(options.attestation_certificate_file_path) + except Exception as e: + raise AdminError(f"While loading the attestation certificate file: {str(e)}") + + # Validate the certificate using the given root authority + # (this should be *one of* Ledger's public keys) + result = att_cert.validate_and_get_values(root_of_trust) + + # powHSM specific validations + if "quote" not in result: + raise AdminError("Certificate does not contain a powHSM attestation") + + powhsm_result = result["quote"] + if not powhsm_result[0]: + raise AdminError( + f"Invalid powHSM attestation: error validating '{powhsm_result[1]}'") + powhsm_result = powhsm_result[1] + + sgx_quote = powhsm_result["sgx_quote"] + powhsm_message = bytes.fromhex(powhsm_result["message"]) + if not PowHsmAttestationMessage.is_header(powhsm_message): + raise AdminError( + f"Invalid powHSM attestation message header: {powhsm_message.hex()}") + + try: + powhsm_message = PowHsmAttestationMessage(powhsm_message) + except Exception as e: + raise AdminError(f"Error parsing powHSM attestation message: {str(e)}") + reported_pubkeys_hash = powhsm_message.public_keys_hash + + if reported_pubkeys_hash != pubkeys_hash: + raise AdminError( + f"powHSM attestation public keys hash mismatch: expected {pubkeys_hash.hex()}" + f" but attestation reports {reported_pubkeys_hash.hex()}" + ) + + signer_info = [ + f"Hash: {pubkeys_hash.hex()}", + "", + f"Installed powHSM MRENCLAVE: {sgx_quote.report_body.mrenclave.hex()}", + f"Installed powHSM MRSIGNER: {sgx_quote.report_body.mrsigner.hex()}", + f"Installed powHSM version: {powhsm_message.version}", + ] + + signer_info += [ + f"Platform: {powhsm_message.platform}", + f"UD value: {powhsm_message.ud_value.hex()}", + f"Best block: {powhsm_message.best_block.hex()}", + f"Last transaction signed: {powhsm_message.last_signed_tx.hex()}", + f"Timestamp: {powhsm_message.timestamp}", + ] + + head( + ["powHSM verified with public keys:"] + pubkeys_output + signer_info, + fill="-", + ) diff --git a/middleware/tests/admin/test_adm_sgx.py b/middleware/tests/admin/test_adm_sgx.py index 53c139cd..ca8427b6 100644 --- a/middleware/tests/admin/test_adm_sgx.py +++ b/middleware/tests/admin/test_adm_sgx.py @@ -40,6 +40,9 @@ def setUp(self): "new_pin": None, "no_unlock": False, "attestation_ud_source": "https://public-node.rsk.co", + "attestation_certificate_file_path": None, + "root_authority": None, + "pubkeys_file_path": None, "operation": None, "output_file_path": None, "pin": None, @@ -61,7 +64,7 @@ def test_unlock(self, do_unlock): call(Namespace(**expected_options)) ] - with patch('sys.argv', ['adm_sgx.py', '-p', 'a-pin', 'unlock']): + with patch('sys.argv', ['adm_sgx.py', '-P', 'a-pin', 'unlock']): with self.assertRaises(SystemExit) as e: main() self.assertEqual(e.exception.code, 0) @@ -89,7 +92,7 @@ def test_onboard(self, do_onboard): ] with patch('sys.argv', - ['adm_sgx.py', '-p', 'a-pin', 'onboard']): + ['adm_sgx.py', '-P', 'a-pin', 'onboard']): with self.assertRaises(SystemExit) as e: main() self.assertEqual(e.exception.code, 0) @@ -119,7 +122,7 @@ def test_pubkeys(self, do_get_pubkeys): call(Namespace(**expected_options)) ] - with patch('sys.argv', ['adm_sgx.py', '-p', 'a-pin', '-o', 'a-path', '-u', + 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() @@ -154,8 +157,8 @@ def test_changepin(self, do_changepin): call(Namespace(**expected_options)) ] - with patch('sys.argv', ['adm_sgx.py', '-p', 'old-pin', '-n', 'new-pin', - '-r', '4567', '-a', 'changepin']): + with patch('sys.argv', ['adm_sgx.py', '-P', 'old-pin', '-n', 'new-pin', + '-p', '4567', '-a', 'changepin']): with self.assertRaises(SystemExit) as e: main() self.assertEqual(e.exception.code, 0) @@ -186,7 +189,7 @@ def test_attestation(self, do_attestation): ] with patch('sys.argv', ['adm_sgx.py', - '-p', 'a-pin', + '-P', 'a-pin', '-o', 'out-path', '--attudsource', 'user-defined-source', 'attestation']): diff --git a/middleware/tests/admin/test_attestation_utils.py b/middleware/tests/admin/test_attestation_utils.py new file mode 100644 index 00000000..790fa505 --- /dev/null +++ b/middleware/tests/admin/test_attestation_utils.py @@ -0,0 +1,269 @@ +# 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. + +from types import SimpleNamespace +import secp256k1 as ec +from unittest import TestCase +from unittest.mock import patch, mock_open +from parameterized import parameterized +from admin.attestation_utils import AdminError, PowHsmAttestationMessage, load_pubkeys, \ + compute_pubkeys_hash, compute_pubkeys_output, \ + get_root_of_trust +from .test_attestation_utils_resources import TEST_PUBKEYS_JSON, \ + TEST_PUBKEYS_JSON_INVALID +import logging + +logging.disable(logging.CRITICAL) + + +class TestPowHsmAttestationMessage(TestCase): + @parameterized.expand([ + ("ok_exact", True, b"POWHSM:5.6::"), + ("ok_longer", True, b"POWHSM:5.3::whatcomesafterwards"), + ("version_mismatch", False, b"POWHSM:4.3::"), + ("shorter", False, b"POWHSM:5.3:"), + ("invalid", False, b"something invalid"), + ]) + def test_is_header(self, _, expected, header): + self.assertEqual(expected, PowHsmAttestationMessage.is_header(header)) + + def test_parse_ok(self): + msg = PowHsmAttestationMessage( + b"POWHSM:5.7::" + + b"abc" + + bytes.fromhex("aa"*32) + + bytes.fromhex("bb"*32) + + bytes.fromhex("cc"*32) + + bytes.fromhex("dd"*8) + + bytes.fromhex("00"*7 + "83") + ) + + self.assertEqual("abc", msg.platform) + self.assertEqual(bytes.fromhex("aa"*32), msg.ud_value) + self.assertEqual(bytes.fromhex("bb"*32), msg.public_keys_hash) + self.assertEqual(bytes.fromhex("cc"*32), msg.best_block) + self.assertEqual(bytes.fromhex("dd"*8), msg.last_signed_tx) + self.assertEqual(0x83, msg.timestamp) + + def test_parse_header_mismatch(self): + with self.assertRaises(ValueError) as e: + PowHsmAttestationMessage( + b"POWHSM:3.0::" + + b"abc" + + bytes.fromhex("aa"*32) + + bytes.fromhex("bb"*32) + + bytes.fromhex("cc"*32) + + bytes.fromhex("dd"*8) + + bytes.fromhex("00"*7 + "83") + + b"0" + ) + self.assertIn("header", str(e.exception)) + + def test_parse_shorter(self): + with self.assertRaises(ValueError) as e: + PowHsmAttestationMessage( + b"POWHSM:5.7::" + + b"abc" + + bytes.fromhex("aa"*32) + + bytes.fromhex("bb"*32) + + bytes.fromhex("cc"*32) + + bytes.fromhex("dd"*8) + + bytes.fromhex("00"*6 + "83") + ) + self.assertIn("length mismatch", str(e.exception)) + + def test_parse_longer(self): + with self.assertRaises(ValueError) as e: + PowHsmAttestationMessage( + b"POWHSM:5.7::" + + b"abc" + + bytes.fromhex("aa"*32) + + bytes.fromhex("bb"*32) + + bytes.fromhex("cc"*32) + + bytes.fromhex("dd"*8) + + bytes.fromhex("00"*7 + "83") + + b"0" + ) + self.assertIn("length mismatch", str(e.exception)) + + +class TestLoadPubKeys(TestCase): + def test_load_pubkeys_ok(self): + with patch("builtins.open", mock_open()) as file_mock: + file_mock.return_value.read.return_value = TEST_PUBKEYS_JSON + pubkeys = load_pubkeys("a-path") + + file_mock.assert_called_with("a-path", "r") + self.assertEqual([ + "m/44'/1'/0'/0/0", + "m/44'/1'/1'/0/0", + "m/44'/1'/2'/0/0", + ], list(pubkeys.keys())) + self.assertEqual(bytes.fromhex( + "03abe31ee7c91976f7a56d8e196d82d5ce75a0fcc2935723bf25610d22bd81e50f"), + pubkeys["m/44'/1'/0'/0/0"].serialize(compressed=True)) + self.assertEqual(bytes.fromhex( + "03d44eac557a58be6cd4a40cbdaa9ed22cf4f0322e8c7bb84f6421d5bdda3b99ff"), + pubkeys["m/44'/1'/1'/0/0"].serialize(compressed=True)) + self.assertEqual(bytes.fromhex( + "02877a756d2b82ddff342fa327b065326001b204b2f86a24ac36638b5162330141"), + pubkeys["m/44'/1'/2'/0/0"].serialize(compressed=True)) + + def test_load_pubkeys_file_doesnotexist(self): + with patch("builtins.open", mock_open()) as file_mock: + file_mock.side_effect = FileNotFoundError("another error") + with self.assertRaises(AdminError) as e: + load_pubkeys("a-path") + file_mock.assert_called_with("a-path", "r") + self.assertIn("another error", str(e.exception)) + + def test_load_pubkeys_invalid_json(self): + with patch("builtins.open", mock_open()) as file_mock: + file_mock.return_value.read.return_value = "not json" + with self.assertRaises(AdminError) as e: + load_pubkeys("a-path") + file_mock.assert_called_with("a-path", "r") + self.assertIn("Unable to read", str(e.exception)) + + def test_load_pubkeys_notamap(self): + with patch("builtins.open", mock_open()) as file_mock: + file_mock.return_value.read.return_value = "[1,2,3]" + with self.assertRaises(AdminError) as e: + load_pubkeys("a-path") + file_mock.assert_called_with("a-path", "r") + self.assertIn("top level", str(e.exception)) + + def test_load_pubkeys_invalid_pubkey(self): + with patch("builtins.open", mock_open()) as file_mock: + file_mock.return_value.read.return_value = TEST_PUBKEYS_JSON_INVALID + with self.assertRaises(AdminError) as e: + load_pubkeys("a-path") + file_mock.assert_called_with("a-path", "r") + self.assertIn("public key", str(e.exception)) + + +class TestComputePubkeysHash(TestCase): + def test_ok(self): + expected_hash = bytes.fromhex( + "ad33c8be1af2520e2c533d883a2021654102917969816cd1b9dacfcccf4e139e") + + def to_pub(h): + return ec.PrivateKey(bytes.fromhex(h), raw=True).pubkey + + keys = { + "1first": to_pub("11"*32), + "3third": to_pub("33"*32), + "2second": to_pub("22"*32), + } + + self.assertEqual(expected_hash, compute_pubkeys_hash(keys)) + + def test_empty_errors(self): + with self.assertRaises(AdminError) as e: + compute_pubkeys_hash({}) + self.assertIn("empty", str(e.exception)) + + +class TestComputePubkeysOutput(TestCase): + def test_sample_output(self): + class PubKey: + def __init__(self, h): + self.h = h + + def serialize(self, compressed): + return bytes.fromhex(self.h) if compressed else "" + + keys = { + "name": PubKey("11223344"), + "longer_name": PubKey("aabbcc"), + "very_very_long_name": PubKey("6677889900"), + } + + self.assertEqual([ + "longer_name: aabbcc", + "name: 11223344", + "very_very_long_name: 6677889900", + ], compute_pubkeys_output(keys)) + + +class TestGetRootOfTrust(TestCase): + @patch("admin.attestation_utils.HSMCertificateV2ElementX509") + @patch("admin.attestation_utils.Path") + def test_file_ok(self, path, HSMCertificateV2ElementX509): + path.return_value.is_file.return_value = True + HSMCertificateV2ElementX509.from_pemfile.return_value = "the-result" + + self.assertEqual("the-result", get_root_of_trust("a-file-path")) + + path.assert_called_with("a-file-path") + HSMCertificateV2ElementX509.from_pemfile.assert_called_with( + "a-file-path", "sgx_root", "sgx_root") + + @patch("admin.attestation_utils.HSMCertificateV2ElementX509") + @patch("admin.attestation_utils.Path") + def test_file_invalid(self, path, HSMCertificateV2ElementX509): + path.return_value.is_file.return_value = True + err = ValueError("something wrong") + HSMCertificateV2ElementX509.from_pemfile.side_effect = err + + with self.assertRaises(ValueError) as e: + get_root_of_trust("a-file-path") + self.assertEqual(err, e.exception) + + path.assert_called_with("a-file-path") + HSMCertificateV2ElementX509.from_pemfile.assert_called_with( + "a-file-path", "sgx_root", "sgx_root") + + @patch("admin.attestation_utils.requests") + @patch("admin.attestation_utils.HSMCertificateV2ElementX509") + @patch("admin.attestation_utils.Path") + def test_url_ok(self, path, HSMCertificateV2ElementX509, requests): + path.return_value.is_file.return_value = False + requests.get.return_value = SimpleNamespace(**{ + "status_code": 200, + "content": b"some-pem", + }) + HSMCertificateV2ElementX509.from_pem.return_value = "the-result" + + self.assertEqual("the-result", get_root_of_trust("a-url")) + + path.assert_called_with("a-url") + requests.get.assert_called_with("a-url") + HSMCertificateV2ElementX509.from_pem.assert_called_with( + "some-pem", "sgx_root", "sgx_root") + + @patch("admin.attestation_utils.requests") + @patch("admin.attestation_utils.HSMCertificateV2ElementX509") + @patch("admin.attestation_utils.Path") + def test_url_error_get(self, path, HSMCertificateV2ElementX509, requests): + path.return_value.is_file.return_value = False + requests.get.return_value = SimpleNamespace(**{ + "status_code": 123, + }) + + with self.assertRaises(RuntimeError) as e: + get_root_of_trust("a-url") + self.assertIn("fetching root of trust", str(e.exception)) + + path.assert_called_with("a-url") + requests.get.assert_called_with("a-url") + HSMCertificateV2ElementX509.from_pem.assert_not_called() diff --git a/middleware/tests/admin/test_attestation_utils_resources.py b/middleware/tests/admin/test_attestation_utils_resources.py new file mode 100644 index 00000000..6da5092d --- /dev/null +++ b/middleware/tests/admin/test_attestation_utils_resources.py @@ -0,0 +1,36 @@ +# 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. + +TEST_PUBKEYS_JSON = """ +{ + "m/44'/1'/0'/0/0": "04abe31ee7c91976f7a56d8e196d82d5ce75a0fcc2935723bf25610d22bd81e50fb4def0b3f99ae2054868ea2133e5b88145220ac492f86b942bd40f574d9117e1", + "m/44'/1'/1'/0/0": "04d44eac557a58be6cd4a40cbdaa9ed22cf4f0322e8c7bb84f6421d5bdda3b99ff73982e67c4550faad3f67de7615a0a32cfcf3322f5eca5cbaa6792131600ca17", + "m/44'/1'/2'/0/0": "04877a756d2b82ddff342fa327b065326001b204b2f86a24ac36638b51623301416076d2eb1a048c2efa3934d5673bdf3db8d0f1e8ade406c6a478f0910cdb8c4c" +} +""" + +TEST_PUBKEYS_JSON_INVALID = """ +{ + "path_1": "02877a756d2b82ddff342fa327b065326001b204b2f86a24ac36638b5162330141", + "path_2": "11223344" +} +""" diff --git a/middleware/tests/admin/test_certificate_v2.py b/middleware/tests/admin/test_certificate_v2.py index ed7c0c8d..99a82099 100644 --- a/middleware/tests/admin/test_certificate_v2.py +++ b/middleware/tests/admin/test_certificate_v2.py @@ -21,6 +21,7 @@ # SOFTWARE. from unittest import TestCase +from unittest.mock import patch from admin.certificate_v1 import HSMCertificate from admin.certificate_v2 import HSMCertificateV2, HSMCertificateV2Element, \ HSMCertificateV2ElementSGXQuote, \ @@ -41,17 +42,34 @@ def test_parse_identity(self): cert = HSMCertificateV2(TEST_CERTIFICATE) self.assertEqual(TEST_CERTIFICATE, cert.to_dict()) - def test_validate_and_get_values_value(self): + @patch("admin.certificate_v2.SgxQuote") + def test_validate_and_get_values_value(self, SgxQuoteMock): + SgxQuoteMock.return_value = "an-sgx-quote" cert = HSMCertificateV2(TEST_CERTIFICATE) self.assertEqual({ "quote": ( - True, - "504f5748534d3a352e343a3a736778f36f7bc09aab50c0886a442b2d04b18186720bd" - "a7a753643066cd0bc0a4191800c4d091913d39750dc8975adbdd261bd10c1c2e110fa" - "a47cfbe30e740895552bbdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6" - "e7191f67ea4209b00000000000000000000000000000000", - None) + True, { + "sgx_quote": "an-sgx-quote", + "message": "504f5748534d3a352e343a3a736778f36f7bc09aab50c0886a442b2" + "d04b18186720bda7a753643066cd0bc0a4191800c4d091913d39750" + "dc8975adbdd261bd10c1c2e110faa47cfbe30e740895552bbdcb3c1" + "7c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea420" + "9b00000000000000000000000000000000", + }, None) }, cert.validate_and_get_values('a-root-of-trust')) + SgxQuoteMock.assert_called_with(bytes.fromhex( + "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34" + "d562f4564fc182000000000e0e100fffff0100000000000000000001000000000000000000" + "00000000000000000000000000000000000000000000050000000000000007000000000000" + "00d32688d3c1f3dfcc8b0b36eac7c89d49af331800bd56248044166fa6699442c100000000" + "00000000000000000000000000000000000000000000000000000000718c2f1a0efbd513e0" + "16fafd6cf62a624442f2d83708d4b33ab5a8d8c1cd4dd00000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000006400010000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000009e95" + "bb875c1a728071f70ad8c9d03f1744c19acb0580921e611ac9104f7701d000000000000000" + "00000000000000000000000000000000000000000000000000")) class TestHSMCertificateV2Element(TestCase): @@ -281,3 +299,29 @@ def test_from_dict_invalid_message(self): "signed_by": "platform_ca" }) self.assertIn("Invalid message", str(e.exception)) + + def test_from_pem(self): + self.assertEqual({ + "name": "thename", + "type": "x509_pem", + "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", + "signed_by": "whosigned", + }, HSMCertificateV2ElementX509.from_pem(""" + -----BEGIN CERTIFICATE----- + dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl + -----END CERTIFICATE----- + """, "thename", "whosigned").to_dict()) + + @patch("admin.certificate_v2.Path") + @patch("admin.certificate_v2.HSMCertificateV2ElementX509.from_pem") + def test_from_pemfile(self, from_pem, Path): + Path.return_value.read_text.return_value = "the pem contents" + from_pem.return_value = "the instance" + self.assertEqual("the instance", + HSMCertificateV2ElementX509.from_pemfile("a-file.pem", + "the name", + "who signed")) + Path.assert_called_with("a-file.pem") + from_pem.assert_called_with("the pem contents", + "the name", + "who signed") diff --git a/middleware/tests/admin/test_verify_ledger_attestation.py b/middleware/tests/admin/test_verify_ledger_attestation.py index 40f510e2..ba5638cc 100644 --- a/middleware/tests/admin/test_verify_ledger_attestation.py +++ b/middleware/tests/admin/test_verify_ledger_attestation.py @@ -22,11 +22,12 @@ from types import SimpleNamespace from unittest import TestCase -from unittest.mock import Mock, call, patch, mock_open +from unittest.mock import Mock, call, patch from admin.misc import AdminError from admin.pubkeys import PATHS from admin.verify_ledger_attestation import do_verify_attestation import ecdsa +import secp256k1 as ec import hashlib import logging @@ -39,7 +40,7 @@ @patch("sys.stdout.write") -class TestVerifyAttestation(TestCase): +class TestVerifyLedgerAttestation(TestCase): def setUp(self): self.certification_path = 'certification-path' self.pubkeys_path = 'pubkeys-path' @@ -60,17 +61,20 @@ def setUp(self): path_name_padding = max(map(len, paths)) for path in sorted(paths): pubkey = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1).get_verifying_key() - self.public_keys[path] = pubkey.to_string('compressed').hex() + self.public_keys[path] = ec.PublicKey( + pubkey.to_string('compressed'), raw=True) pubkeys_hash.update(pubkey.to_string('uncompressed')) self.expected_pubkeys_output.append( f"{(path + ':').ljust(path_name_padding+1)} " f"{pubkey.to_string('compressed').hex()}" ) self.pubkeys_hash = pubkeys_hash.digest() + self.expected_ui_pubkey = self.public_keys[EXPECTED_UI_DERIVATION_PATH]\ + .serialize(compressed=True).hex() self.ui_msg = UI_HEADER + \ bytes.fromhex("aa"*32) + \ - bytes.fromhex("bb"*33) + \ + bytes.fromhex(self.expected_ui_pubkey) + \ bytes.fromhex("cc"*32) + \ bytes.fromhex("0123") self.ui_hash = bytes.fromhex("ee" * 32) @@ -81,7 +85,7 @@ def setUp(self): bytes.fromhex(self.pubkeys_hash.hex()) + \ bytes.fromhex('bb'*32) + \ bytes.fromhex('cc'*8) + \ - bytes.fromhex('dd'*8) + bytes.fromhex('00'*7 + 'ab') self.signer_hash = bytes.fromhex("ff" * 32) @@ -91,9 +95,9 @@ def setUp(self): @patch("admin.verify_ledger_attestation.head") @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_legacy(self, - loads_mock, + load_pubkeys_mock, certificate_mock, head_mock, _): self.signer_msg = LEGACY_SIGNER_HEADER + \ @@ -101,15 +105,14 @@ def test_verify_attestation_legacy(self, self.signer_hash = bytes.fromhex("ff" * 32) self.result['signer'] = (True, self.signer_msg.hex(), self.signer_hash.hex()) - loads_mock.return_value = self.public_keys + load_pubkeys_mock.return_value = self.public_keys att_cert = Mock() att_cert.validate_and_get_values = Mock(return_value=self.result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - do_verify_attestation(self.default_options) + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual([call(self.certification_path)], certificate_mock.from_jsonfile.call_args_list) @@ -117,7 +120,8 @@ def test_verify_attestation_legacy(self, [ "UI verified with:", f"UD value: {'aa'*32}", - f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): {'bb'*33}", + f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): " + f"{self.expected_ui_pubkey}", f"Authorized signer hash: {'cc'*32}", "Authorized signer iteration: 291", f"Installed UI hash: {'ee'*32}", @@ -140,21 +144,19 @@ def test_verify_attestation_legacy(self, @patch("admin.verify_ledger_attestation.head") @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation(self, - loads_mock, + load_pubkeys_mock, certificate_mock, - head_mock, - _): - loads_mock.return_value = self.public_keys + head_mock, _): + load_pubkeys_mock.return_value = self.public_keys att_cert = Mock() att_cert.validate_and_get_values = Mock(return_value=self.result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - do_verify_attestation(self.default_options) + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual([call(self.certification_path)], certificate_mock.from_jsonfile.call_args_list) @@ -162,7 +164,8 @@ def test_verify_attestation(self, [ "UI verified with:", f"UD value: {'aa'*32}", - f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): {'bb'*33}", + f"Derived public key ({EXPECTED_UI_DERIVATION_PATH}): " + f"{self.expected_ui_pubkey}", f"Authorized signer hash: {'cc'*32}", "Authorized signer iteration: 291", f"Installed UI hash: {'ee'*32}", @@ -185,7 +188,7 @@ def test_verify_attestation(self, "Best block: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "bbbbb", "Last transaction signed: cccccccccccccccc", - "Timestamp: dddddddddddddddd", + "Timestamp: 171", ], fill="-", ) @@ -206,66 +209,43 @@ def test_verify_attestation_no_pubkey(self, _): do_verify_attestation(options) self.assertEqual('No public keys file given', str(e.exception)) - @patch("json.loads") - def test_verify_attestation_invalid_pubkeys_map(self, loads_mock, _): - loads_mock.return_value = 'invalid-json' - with patch('builtins.open', mock_open(read_data='')): - with self.assertRaises(ValueError) as e: - do_verify_attestation(self.default_options) - - self.assertEqual(('Unable to read public keys from "pubkeys-path": Public keys ' - 'file must contain an object as a top level element'), - str(e.exception)) - - @patch("json.loads") - def test_verify_attestation_invalid_pubkey(self, loads_mock, _): - loads_mock.return_value = {'invalid-path': 'invalid-key'} - with patch('builtins.open', mock_open(read_data='')): - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) - - self.assertEqual('Invalid public key for path invalid-path: invalid-key', - str(e.exception)) - - @patch("json.loads") - def test_verify_attestation_no_ui_derivation_key(self, loads_mock, _): + @patch("admin.verify_ledger_attestation.load_pubkeys") + def test_verify_attestation_no_ui_derivation_key(self, load_pubkeys_mock, _): incomplete_pubkeys = self.public_keys incomplete_pubkeys.pop(EXPECTED_UI_DERIVATION_PATH, None) - loads_mock.return_value = incomplete_pubkeys + load_pubkeys_mock.return_value = incomplete_pubkeys - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual((f'Public key with path {EXPECTED_UI_DERIVATION_PATH} ' 'not present in public key file'), str(e.exception)) @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_invalid_certificate(self, - loads_mock, + load_pubkeys_mock, certificate_mock, _): - loads_mock.return_value = self.public_keys + load_pubkeys_mock.return_value = self.public_keys certificate_mock.from_jsonfile = Mock(side_effect=Exception('error-msg')) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual('While loading the attestation certificate file: error-msg', str(e.exception)) @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_no_ui_att(self, - loads_mock, + load_pubkeys_mock, certificate_mock, _): - loads_mock.return_value = self.public_keys + load_pubkeys_mock.return_value = self.public_keys result = self.result result.pop('ui', None) @@ -273,115 +253,106 @@ def test_verify_attestation_no_ui_att(self, att_cert.validate_and_get_values = Mock(return_value=self.result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual('Certificate does not contain a UI attestation', str(e.exception)) @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_invalid_ui_att(self, - loads_mock, + load_pubkeys_mock, certificate_mock, _): - loads_mock.return_value = self.public_keys + load_pubkeys_mock.return_value = self.public_keys result = self.result result['ui'] = (False, 'ui') att_cert = Mock() att_cert.validate_and_get_values = Mock(return_value=result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual("Invalid UI attestation: error validating 'ui'", str(e.exception)) @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_no_signer_att(self, - loads_mock, + load_pubkeys_mock, certificate_mock, _): - loads_mock.return_value = self.public_keys - + load_pubkeys_mock.return_value = self.public_keys result = self.result result.pop('signer', None) att_cert = Mock() att_cert.validate_and_get_values = Mock(return_value=self.result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual('Certificate does not contain a Signer attestation', str(e.exception)) @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_invalid_signer_att(self, - loads_mock, + load_pubkeys_mock, certificate_mock, _): - loads_mock.return_value = self.public_keys + load_pubkeys_mock.return_value = self.public_keys result = self.result result['signer'] = (False, 'signer') att_cert = Mock() att_cert.validate_and_get_values = Mock(return_value=result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual(("Invalid Signer attestation: error validating 'signer'"), str(e.exception)) @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_invalid_signer_att_header(self, - loads_mock, + load_pubkeys_mock, certificate_mock, _): - loads_mock.return_value = self.public_keys + load_pubkeys_mock.return_value = self.public_keys signer_header = b"POWHSM:AAA::somerandomstuff".hex() self.result["signer"] = (True, signer_header, self.signer_hash.hex()) att_cert = Mock() att_cert.validate_and_get_values = Mock(return_value=self.result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) self.assertEqual((f"Invalid Signer attestation message header: {signer_header}"), str(e.exception)) @patch("admin.verify_ledger_attestation.HSMCertificate") - @patch("json.loads") + @patch("admin.verify_ledger_attestation.load_pubkeys") def test_verify_attestation_invalid_signer_att_msg_too_long(self, - loads_mock, + load_pubkeys_mock, certificate_mock, _): - loads_mock.return_value = self.public_keys + load_pubkeys_mock.return_value = self.public_keys signer_header = (b"POWHSM:5.9::" + b"aa"*300).hex() self.result["signer"] = (True, signer_header, self.signer_hash.hex()) att_cert = Mock() att_cert.validate_and_get_values = Mock(return_value=self.result) certificate_mock.from_jsonfile = Mock(return_value=att_cert) - with patch('builtins.open', mock_open(read_data='')) as file_mock: - with self.assertRaises(AdminError) as e: - do_verify_attestation(self.default_options) + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.default_options) - self.assertEqual([call(self.pubkeys_path, 'r')], file_mock.call_args_list) - self.assertEqual(("Signer attestation message longer " - f"than expected: {signer_header}"), - str(e.exception)) + load_pubkeys_mock.assert_called_with(self.pubkeys_path) + self.assertIn("Signer attestation message length mismatch", str(e.exception)) diff --git a/middleware/tests/admin/test_verify_sgx_attestation.py b/middleware/tests/admin/test_verify_sgx_attestation.py new file mode 100644 index 00000000..5bacadf5 --- /dev/null +++ b/middleware/tests/admin/test_verify_sgx_attestation.py @@ -0,0 +1,241 @@ +# 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. + +from types import SimpleNamespace +from unittest import TestCase +from unittest.mock import Mock, patch +from parameterized import parameterized +from admin.misc import AdminError +from admin.pubkeys import PATHS +from admin.verify_sgx_attestation import do_verify_attestation, DEFAULT_ROOT_AUTHORITY +import ecdsa +import secp256k1 as ec +import hashlib +import logging + +logging.disable(logging.CRITICAL) + + +@patch("sys.stdout.write") +@patch("admin.verify_sgx_attestation.head") +@patch("admin.verify_sgx_attestation.HSMCertificate") +@patch("admin.verify_sgx_attestation.load_pubkeys") +@patch("admin.verify_sgx_attestation.get_root_of_trust") +class TestVerifySgxAttestation(TestCase): + def setUp(self): + self.certification_path = 'certification-path' + self.pubkeys_path = 'pubkeys-path' + self.options = SimpleNamespace(**{ + 'attestation_certificate_file_path': self.certification_path, + 'pubkeys_file_path': self.pubkeys_path, + 'root_authority': None + }) + + paths = [] + for path in PATHS.values(): + paths.append(str(path)) + + self.public_keys = {} + self.expected_pubkeys_output = [] + pubkeys_hash = hashlib.sha256() + path_name_padding = max(map(len, paths)) + for path in sorted(paths): + pubkey = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1).get_verifying_key() + self.public_keys[path] = ec.PublicKey( + pubkey.to_string('compressed'), raw=True) + pubkeys_hash.update(pubkey.to_string('uncompressed')) + self.expected_pubkeys_output.append( + f"{(path + ':').ljust(path_name_padding+1)} " + f"{pubkey.to_string('compressed').hex()}" + ) + self.expected_pubkeys_hash = pubkeys_hash.digest().hex() + + self.powhsm_msg = \ + b"POWHSM:5.4::" + \ + b'plf' + \ + bytes.fromhex('aa'*32) + \ + bytes.fromhex(self.expected_pubkeys_hash) + \ + bytes.fromhex('bb'*32) + \ + bytes.fromhex('cc'*8) + \ + bytes.fromhex('00'*7 + 'cd') + + self.mock_sgx_quote = SimpleNamespace(**{ + "report_body": SimpleNamespace(**{ + "mrenclave": bytes.fromhex("aabbccdd"), + "mrsigner": bytes.fromhex("1122334455"), + }) + }) + + self.validate_result = {"quote": ( + True, { + "sgx_quote": self.mock_sgx_quote, + "message": self.powhsm_msg.hex() + }, None) + } + + def configure_mocks(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head): + get_root_of_trust.return_value = "the-root-of-trust" + load_pubkeys.return_value = self.public_keys + self.mock_certificate = Mock() + self.mock_certificate.validate_and_get_values.return_value = self.validate_result + HSMCertificate.from_jsonfile.return_value = self.mock_certificate + + @parameterized.expand([ + ("default_root", None), + ("custom_root", "a-custom-root") + ]) + def test_verify_attestation(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _, __, custom_root): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + if custom_root: + self.options.root_authority = custom_root + + do_verify_attestation(self.options) + + if custom_root: + get_root_of_trust.assert_called_with(custom_root) + else: + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) + self.mock_certificate.validate_and_get_values \ + .assert_called_with("the-root-of-trust") + head.assert_called_with([ + "powHSM verified with public keys:" + ] + self.expected_pubkeys_output + [ + f"Hash: {self.expected_pubkeys_hash}", + "", + "Installed powHSM MRENCLAVE: aabbccdd", + "Installed powHSM MRSIGNER: 1122334455", + "Installed powHSM version: 5.4", + "Platform: plf", + f"UD value: {"aa"*32}", + f"Best block: {"bb"*32}", + f"Last transaction signed: {"cc"*8}", + "Timestamp: 205", + ], fill="-") + + def test_verify_attestation_err_load_pubkeys(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + load_pubkeys.side_effect = ValueError("pubkeys error") + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("pubkeys error", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_not_called() + self.mock_certificate.validate_and_get_values.assert_not_called() + + def test_verify_attestation_err_load_cert(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + HSMCertificate.from_jsonfile.side_effect = ValueError("load cert error") + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("load cert error", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) + self.mock_certificate.validate_and_get_values.assert_not_called() + + def test_verify_attestation_validation_noquote(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + self.mock_certificate.validate_and_get_values.return_value = {"something": "else"} + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("does not contain", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) + self.mock_certificate.validate_and_get_values \ + .assert_called_with("the-root-of-trust") + + def test_verify_attestation_validation_failed(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + self.mock_certificate.validate_and_get_values.return_value = { + "quote": (False, "a validation error") + } + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("validation error", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) + self.mock_certificate.validate_and_get_values \ + .assert_called_with("the-root-of-trust") + + def test_verify_attestation_invalid_header(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + self.validate_result["quote"][1]["message"] = "aabbccdd" + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("message header", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) + self.mock_certificate.validate_and_get_values \ + .assert_called_with("the-root-of-trust") + + def test_verify_attestation_invalid_message(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + self.validate_result["quote"][1]["message"] = b"POWHSM:5.4::plf".hex() + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("parsing", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) + self.mock_certificate.validate_and_get_values \ + .assert_called_with("the-root-of-trust") + + def test_verify_attestation_pkh_mismatch(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + self.public_keys.popitem() + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("hash mismatch", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + load_pubkeys.assert_called_with(self.pubkeys_path) + HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) + self.mock_certificate.validate_and_get_values \ + .assert_called_with("the-root-of-trust") diff --git a/setup.cfg b/setup.cfg index 36a420ae..1dbd51f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ per-file-ignores = middleware/admin/certificate.py:F401, middleware/tests/sgx/test_envelope.py:E122, middleware/tests/admin/test_certificate_v2_resources.py:E501, + middleware/tests/admin/test_attestation_utils_resources.py:E501, show-source = False statistics = True From 0b62d17a614f5840a4d136dcc98c748f57d48f56 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Wed, 1 Jan 2025 00:32:30 +1300 Subject: [PATCH 13/21] Certificate V2 sgx_quote type element validation (#234) - HSMCertificateV2Element base class now raises not implemented errors for is_valid and get_pubkey - Implemented is_valid in HSMCertificateV2ElementSGXQuote - HSMCertificateV2ElementSGXQuote's message method now returns an SgxQuote - Mocking is_valid and get_pubkey in the rest of HSMCertificateV2Element's subclasses - Added and updated unit tests --- middleware/admin/certificate_v2.py | 44 ++++- middleware/tests/admin/test_certificate_v2.py | 183 ++++++++++++++---- 2 files changed, 185 insertions(+), 42 deletions(-) diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index e84591d2..f5bf6a2e 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -23,6 +23,8 @@ import re from pathlib import Path import base64 +import ecdsa +import hashlib from .certificate_v1 import HSMCertificate from .utils import is_nonempty_hex_string from sgx.envelope import SgxQuote @@ -62,14 +64,10 @@ def get_value(self): raise NotImplementedError(f"{type(self).__name__} can't provide a value") def get_pubkey(self): - # TODO: this should yield not implemented - # TODO: implementation should be down to each specific subclass - return None + raise NotImplementedError(f"{type(self).__name__} can't provide a public key") def is_valid(self, certifier): - # TODO: this should yield not implemented - # TODO: implementation should be down to each specific subclass - return True + raise NotImplementedError(f"{type(self).__name__} can't be queried for validity") def get_tweak(self): return None @@ -97,7 +95,7 @@ def _init_with_map(self, element_map): @property def message(self): - return self._message.hex() + return SgxQuote(self._message) @property def custom_data(self): @@ -107,9 +105,25 @@ def custom_data(self): def signature(self): return self._signature.hex() + def is_valid(self, certifier): + try: + # Validate custom data + expected = hashlib.sha256(self._custom_data).digest() + if expected != self.message.report_body.report_data.field[:len(expected)]: + return False + + # Verify signature against the certifier + return certifier.get_pubkey().verify_digest( + self._signature, + hashlib.sha256(self._message).digest(), + ecdsa.util.sigdecode_der, + ) + except Exception: + return False + def get_value(self): return { - "sgx_quote": SgxQuote(self._message), + "sgx_quote": self.message, "message": self.custom_data, } @@ -117,7 +131,7 @@ def to_dict(self): return { "name": self.name, "type": "sgx_quote", - "message": self.message, + "message": self._message.hex(), "custom_data": self.custom_data, "signature": self.signature, "signed_by": self.signed_by, @@ -163,6 +177,12 @@ def auth_data(self): def signature(self): return self._signature.hex() + def is_valid(self, certifier): + return True + + def get_pubkey(self): + return ecdsa.VerifyingKey.from_string(self._key, ecdsa.NIST256p) + def to_dict(self): return { "name": self.name, @@ -206,6 +226,12 @@ def _init_with_map(self, element_map): def message(self): return base64.b64encode(self._message).decode("ASCII") + def is_valid(self, certifier): + return True + + def get_pubkey(self): + return None + def to_dict(self): return { "name": self.name, diff --git a/middleware/tests/admin/test_certificate_v2.py b/middleware/tests/admin/test_certificate_v2.py index 99a82099..ae292645 100644 --- a/middleware/tests/admin/test_certificate_v2.py +++ b/middleware/tests/admin/test_certificate_v2.py @@ -20,9 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import ecdsa +import hashlib from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch +from parameterized import parameterized from admin.certificate_v1 import HSMCertificate +from sgx.envelope import SgxQuote from admin.certificate_v2 import HSMCertificateV2, HSMCertificateV2Element, \ HSMCertificateV2ElementSGXQuote, \ HSMCertificateV2ElementSGXAttestationKey, \ @@ -36,43 +40,62 @@ def test_behavior_inherited(self): def test_create_empty_certificate_ok(self): cert = HSMCertificateV2() - self.assertEqual({'version': 2, 'targets': [], 'elements': []}, cert.to_dict()) + self.assertEqual({"version": 2, "targets": [], "elements": []}, cert.to_dict()) def test_parse_identity(self): cert = HSMCertificateV2(TEST_CERTIFICATE) self.assertEqual(TEST_CERTIFICATE, cert.to_dict()) - @patch("admin.certificate_v2.SgxQuote") - def test_validate_and_get_values_value(self, SgxQuoteMock): - SgxQuoteMock.return_value = "an-sgx-quote" + def mock_element(self, which_one_invalid): + class MockElement: + def __init__(self, d): + self.d = d + self.name = d["name"] + self.signed_by = d["signed_by"] + + def is_valid(self, c): + return self.name != which_one_invalid + + def get_value(self): + return f"the value for {self.name}" + + def get_tweak(self): + return None + + def mock_element_factory(k, d): + return MockElement(d) + + HSMCertificateV2.ELEMENT_FACTORY = mock_element_factory + + def test_validate_and_get_values_value(self): + self.mock_element(True) + cert = HSMCertificateV2(TEST_CERTIFICATE) + self.assertEqual({ + "quote": (True, "the value for quote", None), + }, cert.validate_and_get_values("a-root-of-trust")) + + @parameterized.expand([ + ("invalid_quote", "quote"), + ("invalid_attestation", "attestation"), + ("invalid_qe", "quoting_enclave"), + ("invalid_plf", "platform_ca"), + ]) + def test_validate_and_get_values_invalid(self, _, invalid_name): + self.mock_element(invalid_name) cert = HSMCertificateV2(TEST_CERTIFICATE) self.assertEqual({ - "quote": ( - True, { - "sgx_quote": "an-sgx-quote", - "message": "504f5748534d3a352e343a3a736778f36f7bc09aab50c0886a442b2" - "d04b18186720bda7a753643066cd0bc0a4191800c4d091913d39750" - "dc8975adbdd261bd10c1c2e110faa47cfbe30e740895552bbdcb3c1" - "7c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea420" - "9b00000000000000000000000000000000", - }, None) - }, cert.validate_and_get_values('a-root-of-trust')) - SgxQuoteMock.assert_called_with(bytes.fromhex( - "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34" - "d562f4564fc182000000000e0e100fffff0100000000000000000001000000000000000000" - "00000000000000000000000000000000000000000000050000000000000007000000000000" - "00d32688d3c1f3dfcc8b0b36eac7c89d49af331800bd56248044166fa6699442c100000000" - "00000000000000000000000000000000000000000000000000000000718c2f1a0efbd513e0" - "16fafd6cf62a624442f2d83708d4b33ab5a8d8c1cd4dd00000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000000000" - "00000000000000006400010000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000009e95" - "bb875c1a728071f70ad8c9d03f1744c19acb0580921e611ac9104f7701d000000000000000" - "00000000000000000000000000000000000000000000000000")) + "quote": (False, invalid_name), + }, cert.validate_and_get_values("a-root-of-trust")) class TestHSMCertificateV2Element(TestCase): + def setUp(self): + class TestElement(HSMCertificateV2Element): + def __init__(self): + pass + + self.instance = TestElement() + def test_from_dict_unknown_type(self): with self.assertRaises(ValueError) as e: HSMCertificateV2Element.from_dict({ @@ -103,12 +126,41 @@ def test_from_dict_no_signed_by(self): }) self.assertIn("Missing certifier", str(e.exception)) + def test_cant_instantiate(self): + with self.assertRaises(NotImplementedError): + HSMCertificateV2Element() + + def test_get_pubkey_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.get_pubkey() + + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.get_value() + + def test_is_valid_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.is_valid("a-certifier") + class TestHSMCertificateV2ElementSGXQuote(TestCase): + TEST_MESSAGE = \ + "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34d562f"\ + "4564fc182000000000e0e100fffff01000000000000000000010000000000000000000000000000"\ + "000000000000000000000000000000000005000000000000000700000000000000d32688d3c1f3d"\ + "fcc8b0b36eac7c89d49af331800bd56248044166fa6699442c10000000000000000000000000000"\ + "000000000000000000000000000000000000718c2f1a0efbd513e016fafd6cf62a624442f2d8370"\ + "8d4b33ab5a8d8c1cd4dd00000000000000000000000000000000000000000000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000640001000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ + "00000000000000000000000005d53b30e22f66979d36721e10ab7722557257a9ef8ba77ec7fe430"\ + "493c3542f90000000000000000000000000000000000000000000000000000000000000000" + def setUp(self): self.elem = HSMCertificateV2ElementSGXQuote({ "name": "thename", - "message": "aabbcc", + "message": self.TEST_MESSAGE, "custom_data": "ddeeff", "signature": "112233", "signed_by": "whosigned", @@ -117,7 +169,9 @@ def setUp(self): def test_props(self): self.assertEqual("thename", self.elem.name) self.assertEqual("whosigned", self.elem.signed_by) - self.assertEqual("aabbcc", self.elem.message) + self.assertIsInstance(self.elem.message, SgxQuote) + self.assertEqual(bytes.fromhex(self.TEST_MESSAGE), + self.elem.message.get_raw_data()) self.assertEqual("ddeeff", self.elem.custom_data) self.assertEqual("112233", self.elem.signature) @@ -125,7 +179,7 @@ def test_dict_ok(self): self.assertEqual({ "name": "thename", "type": "sgx_quote", - "message": "aabbcc", + "message": self.TEST_MESSAGE, "custom_data": "ddeeff", "signature": "112233", "signed_by": "whosigned", @@ -154,7 +208,7 @@ def test_from_dict_invalid_custom_data(self): HSMCertificateV2Element.from_dict({ "name": "quote", "type": "sgx_quote", - "message": "aabbccdd", + "message": self.TEST_MESSAGE, "custom_data": "not-hex", "signature": "445566778899", "signed_by": "attestation" @@ -166,13 +220,68 @@ def test_from_dict_invalid_signature(self): HSMCertificateV2Element.from_dict({ "name": "quote", "type": "sgx_quote", - "message": "aabbccdd", + "message": self.TEST_MESSAGE, "custom_data": "112233", "signature": "not-hex", "signed_by": "attestation" }) self.assertIn("Invalid signature", str(e.exception)) + def test_get_pubkey_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_pubkey() + + def test_is_valid_ok(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "10061982", + "signature": pk.sign_digest( + hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertTrue(valid_elem.is_valid(certifier)) + + def test_is_valid_custom_data_mismatch(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "11061982", + "signature": pk.sign_digest( + hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertFalse(valid_elem.is_valid(certifier)) + + def test_is_valid_signature_mismatch(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "10061982", + "signature": pk.sign_digest( + hashlib.sha256(b"something else").digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertFalse(valid_elem.is_valid(certifier)) + class TestHSMCertificateV2ElementSGXAttestationKey(TestCase): def setUp(self): @@ -262,6 +371,10 @@ def test_from_dict_invalid_signature(self): }) self.assertIn("Invalid signature", str(e.exception)) + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_value() + class TestHSMCertificateV2ElementX509(TestCase): def setUp(self): @@ -300,6 +413,10 @@ def test_from_dict_invalid_message(self): }) self.assertIn("Invalid message", str(e.exception)) + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_value() + def test_from_pem(self): self.assertEqual({ "name": "thename", From baacb37576a3baad72831aa2740519f59c85d88b Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Sat, 4 Jan 2025 01:50:25 +1300 Subject: [PATCH 14/21] Certificate V2 sgx_attestation_key type element validation (#235) - Implemented HSMCertificateV2ElementSGXAttestationKey's key and is_valid methods - HSMCertificateV2ElementSGXAttestationKey's message now returns an instance of SgxReportBody - HSMCertificateV2ElementX509 now parses the raw certificate into an cryptography x509 certificate object - Implemented HSMCertificateV2ElementX509's get_pubkey method - Added cryptography dependency to middleware docker image - Added and updated unit tests - Incidentally removing some garbage --- docker/mware/requirements.txt | 30 ++ middleware/admin/certificate_v2.py | 56 ++- middleware/da.json | 20 - middleware/tests/admin/test_certificate_v2.py | 365 +----------------- .../admin/test_certificate_v2_element.py | 79 ++++ ...tificate_v2_element_sgx_attestation_key.py | 168 ++++++++ .../test_certificate_v2_element_sgx_quote.py | 170 ++++++++ .../admin/test_certificate_v2_element_x509.py | 200 ++++++++++ 8 files changed, 695 insertions(+), 393 deletions(-) delete mode 100644 middleware/da.json create mode 100644 middleware/tests/admin/test_certificate_v2_element.py create mode 100644 middleware/tests/admin/test_certificate_v2_element_sgx_attestation_key.py create mode 100644 middleware/tests/admin/test_certificate_v2_element_sgx_quote.py create mode 100644 middleware/tests/admin/test_certificate_v2_element_x509.py diff --git a/docker/mware/requirements.txt b/docker/mware/requirements.txt index df27073a..eff05dbf 100644 --- a/docker/mware/requirements.txt +++ b/docker/mware/requirements.txt @@ -810,3 +810,33 @@ yapf==0.40.2 \ zipp==3.19.2 \ --hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \ --hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c +cryptography==44.0.0 \ + --hash=sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7 \ + --hash=sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731 \ + --hash=sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b \ + --hash=sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc \ + --hash=sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543 \ + --hash=sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385 \ + --hash=sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c \ + --hash=sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591 \ + --hash=sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede \ + --hash=sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb \ + --hash=sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f \ + --hash=sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123 \ + --hash=sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c \ + --hash=sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba \ + --hash=sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c \ + --hash=sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285 \ + --hash=sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd \ + --hash=sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092 \ + --hash=sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa \ + --hash=sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289 \ + --hash=sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02 \ + --hash=sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64 \ + --hash=sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053 \ + --hash=sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417 \ + --hash=sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e \ + --hash=sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e \ + --hash=sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7 \ + --hash=sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756 \ + --hash=sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4 diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index f5bf6a2e..c61faa3c 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -25,9 +25,12 @@ import base64 import ecdsa import hashlib +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding +from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 from .certificate_v1 import HSMCertificate from .utils import is_nonempty_hex_string -from sgx.envelope import SgxQuote +from sgx.envelope import SgxQuote, SgxReportBody class HSMCertificateV2Element: @@ -163,11 +166,11 @@ def _init_with_map(self, element_map): @property def message(self): - return self._message.hex() + return SgxReportBody(self._message) @property def key(self): - return self._key.hex() + return ecdsa.VerifyingKey.from_string(self._key, ecdsa.NIST256p) @property def auth_data(self): @@ -178,7 +181,20 @@ def signature(self): return self._signature.hex() def is_valid(self, certifier): - return True + try: + # Validate report data + expected = hashlib.sha256(self.key.to_string() + self._auth_data).digest() + if expected != self.message.report_data.field[:len(expected)]: + return False + + # Verify signature against the certifier + return certifier.get_pubkey().verify_digest( + self._signature, + hashlib.sha256(self._message).digest(), + ecdsa.util.sigdecode_der, + ) + except Exception: + return False def get_pubkey(self): return ecdsa.VerifyingKey.from_string(self._key, ecdsa.NIST256p) @@ -187,8 +203,8 @@ def to_dict(self): return { "name": self.name, "type": "sgx_attestation_key", - "message": self.message, - "key": self.key, + "message": self.message.get_raw_data().hex(), + "key": self.key.to_string("uncompressed").hex(), "auth_data": self.auth_data, "signature": self.signature, "signed_by": self.signed_by, @@ -196,6 +212,9 @@ def to_dict(self): class HSMCertificateV2ElementX509(HSMCertificateV2Element): + HEADER_BEGIN = "-----BEGIN CERTIFICATE-----" + HEADER_END = "-----END CERTIFICATE-----" + @classmethod def from_pemfile(kls, pem_path, name, signed_by): return kls.from_pem(Path(pem_path).read_text(), name, signed_by) @@ -205,14 +224,15 @@ def from_pem(kls, pem_str, name, signed_by): return kls({ "name": name, "message": re.sub(r"[\s\n\r]+", " ", pem_str) - .replace("-----END CERTIFICATE-----", "") - .replace("-----BEGIN CERTIFICATE-----", "") + .replace(kls.HEADER_END, "") + .replace(kls.HEADER_BEGIN, "") .strip().encode(), "signed_by": signed_by, }) def __init__(self, element_map): self._init_with_map(element_map) + self._certificate = None def _init_with_map(self, element_map): super()._init_with_map(element_map) @@ -226,11 +246,29 @@ def _init_with_map(self, element_map): def message(self): return base64.b64encode(self._message).decode("ASCII") + @property + def certificate(self): + if self._certificate is None: + self._certificate = x509.load_pem_x509_certificate(( + self.HEADER_BEGIN + self.message + self.HEADER_END).encode()) + return self._certificate + def is_valid(self, certifier): return True def get_pubkey(self): - return None + try: + public_key = self.certificate.public_key() + + if not isinstance(public_key.curve, SECP256R1): + raise ValueError("Certificate does not have a NIST P-256 public key") + + public_bytes = public_key.public_bytes( + Encoding.X962, PublicFormat.CompressedPoint) + + return ecdsa.VerifyingKey.from_string(public_bytes, ecdsa.NIST256p) + except Exception as e: + raise ValueError(f"Error gathering public key from certificate: {str(e)}") def to_dict(self): return { diff --git a/middleware/da.json b/middleware/da.json deleted file mode 100644 index 104f9c32..00000000 --- a/middleware/da.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 1, - "targets": [ - "attestation" - ], - "elements": [ - { - "name": "attestation", - "message": "ff044e11fa744fcb5c5a22f71558982272f4fe7ca38219c32c4c790f98f7981b1bc63f84b966ffa0188a8ea74b2831cbf258673bce3e4f76dec3c57e2d790aa437e8", - "signature": "304402205d2084fd806f2f1f043225289cd543f48a87d41d5fc446e44f83971efe584c6c0220769573a7e8348ccc1c9ef68b6b4ec9562f305869fb87f211116a71417e45441d", - "signed_by": "device" - }, - { - "name": "device", - "message": "0210b48081be20280434a28e4185e735964a36b5cd8817cbdde534f2839f04c5f998927a36f08343726de175327fa5272e3929b9c357f36f2128c92e14af359ce0e00734d2c93f4c07", - "signature": "30440220181d61b12165b0dd0548cb574577d9f9419a894da56e5b1323375c3b9435622a0220290a29b2a06bbd481b0d0587abadddee39c002ed7f269ac11b23917e7c5c615e", - "signed_by": "root" - } - ] -} diff --git a/middleware/tests/admin/test_certificate_v2.py b/middleware/tests/admin/test_certificate_v2.py index ae292645..3ac67838 100644 --- a/middleware/tests/admin/test_certificate_v2.py +++ b/middleware/tests/admin/test_certificate_v2.py @@ -20,17 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import ecdsa -import hashlib from unittest import TestCase -from unittest.mock import Mock, patch from parameterized import parameterized from admin.certificate_v1 import HSMCertificate -from sgx.envelope import SgxQuote -from admin.certificate_v2 import HSMCertificateV2, HSMCertificateV2Element, \ - HSMCertificateV2ElementSGXQuote, \ - HSMCertificateV2ElementSGXAttestationKey, \ - HSMCertificateV2ElementX509 +from admin.certificate_v2 import HSMCertificateV2 from .test_certificate_v2_resources import TEST_CERTIFICATE @@ -86,359 +79,3 @@ def test_validate_and_get_values_invalid(self, _, invalid_name): self.assertEqual({ "quote": (False, invalid_name), }, cert.validate_and_get_values("a-root-of-trust")) - - -class TestHSMCertificateV2Element(TestCase): - def setUp(self): - class TestElement(HSMCertificateV2Element): - def __init__(self): - pass - - self.instance = TestElement() - - def test_from_dict_unknown_type(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "a-strange-name", - "type": "an-unknown-type", - "some": "other", - "random": "attributes", - }) - self.assertIn("a-strange-name", str(e.exception)) - - def test_from_dict_no_name(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "type": "sgx_quote", - "signed_by": "a-signer", - "some": "other", - "random": "attributes", - }) - self.assertIn("Missing name", str(e.exception)) - - def test_from_dict_no_signed_by(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "a name", - "type": "sgx_quote", - "some": "other", - "random": "attributes", - }) - self.assertIn("Missing certifier", str(e.exception)) - - def test_cant_instantiate(self): - with self.assertRaises(NotImplementedError): - HSMCertificateV2Element() - - def test_get_pubkey_notimplemented(self): - with self.assertRaises(NotImplementedError): - self.instance.get_pubkey() - - def test_get_value_notimplemented(self): - with self.assertRaises(NotImplementedError): - self.instance.get_value() - - def test_is_valid_notimplemented(self): - with self.assertRaises(NotImplementedError): - self.instance.is_valid("a-certifier") - - -class TestHSMCertificateV2ElementSGXQuote(TestCase): - TEST_MESSAGE = \ - "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34d562f"\ - "4564fc182000000000e0e100fffff01000000000000000000010000000000000000000000000000"\ - "000000000000000000000000000000000005000000000000000700000000000000d32688d3c1f3d"\ - "fcc8b0b36eac7c89d49af331800bd56248044166fa6699442c10000000000000000000000000000"\ - "000000000000000000000000000000000000718c2f1a0efbd513e016fafd6cf62a624442f2d8370"\ - "8d4b33ab5a8d8c1cd4dd00000000000000000000000000000000000000000000000000000000000"\ - "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ - "0000000000000000000000000000000000000000000000000000000640001000000000000000000"\ - "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ - "00000000000000000000000005d53b30e22f66979d36721e10ab7722557257a9ef8ba77ec7fe430"\ - "493c3542f90000000000000000000000000000000000000000000000000000000000000000" - - def setUp(self): - self.elem = HSMCertificateV2ElementSGXQuote({ - "name": "thename", - "message": self.TEST_MESSAGE, - "custom_data": "ddeeff", - "signature": "112233", - "signed_by": "whosigned", - }) - - def test_props(self): - self.assertEqual("thename", self.elem.name) - self.assertEqual("whosigned", self.elem.signed_by) - self.assertIsInstance(self.elem.message, SgxQuote) - self.assertEqual(bytes.fromhex(self.TEST_MESSAGE), - self.elem.message.get_raw_data()) - self.assertEqual("ddeeff", self.elem.custom_data) - self.assertEqual("112233", self.elem.signature) - - def test_dict_ok(self): - self.assertEqual({ - "name": "thename", - "type": "sgx_quote", - "message": self.TEST_MESSAGE, - "custom_data": "ddeeff", - "signature": "112233", - "signed_by": "whosigned", - }, self.elem.to_dict()) - - def test_parse_identity(self): - source = TEST_CERTIFICATE["elements"][0] - elem = HSMCertificateV2Element.from_dict(source) - self.assertTrue(isinstance(elem, HSMCertificateV2ElementSGXQuote)) - self.assertEqual(source, elem.to_dict()) - - def test_from_dict_invalid_message(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "quote", - "type": "sgx_quote", - "message": "not-hex", - "custom_data": "112233", - "signature": "445566778899", - "signed_by": "attestation" - }) - self.assertIn("Invalid message", str(e.exception)) - - def test_from_dict_invalid_custom_data(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "quote", - "type": "sgx_quote", - "message": self.TEST_MESSAGE, - "custom_data": "not-hex", - "signature": "445566778899", - "signed_by": "attestation" - }) - self.assertIn("Invalid custom data", str(e.exception)) - - def test_from_dict_invalid_signature(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "quote", - "type": "sgx_quote", - "message": self.TEST_MESSAGE, - "custom_data": "112233", - "signature": "not-hex", - "signed_by": "attestation" - }) - self.assertIn("Invalid signature", str(e.exception)) - - def test_get_pubkey_notimplemented(self): - with self.assertRaises(NotImplementedError): - self.elem.get_pubkey() - - def test_is_valid_ok(self): - pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) - certifier = Mock() - certifier.get_pubkey.return_value = pk.verifying_key - - valid_elem = HSMCertificateV2ElementSGXQuote({ - "name": "thename", - "message": self.TEST_MESSAGE, - "custom_data": "10061982", - "signature": pk.sign_digest( - hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), - sigencode=ecdsa.util.sigencode_der - ).hex(), - "signed_by": "whosigned", - }) - self.assertTrue(valid_elem.is_valid(certifier)) - - def test_is_valid_custom_data_mismatch(self): - pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) - certifier = Mock() - certifier.get_pubkey.return_value = pk.verifying_key - - valid_elem = HSMCertificateV2ElementSGXQuote({ - "name": "thename", - "message": self.TEST_MESSAGE, - "custom_data": "11061982", - "signature": pk.sign_digest( - hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), - sigencode=ecdsa.util.sigencode_der - ).hex(), - "signed_by": "whosigned", - }) - self.assertFalse(valid_elem.is_valid(certifier)) - - def test_is_valid_signature_mismatch(self): - pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) - certifier = Mock() - certifier.get_pubkey.return_value = pk.verifying_key - - valid_elem = HSMCertificateV2ElementSGXQuote({ - "name": "thename", - "message": self.TEST_MESSAGE, - "custom_data": "10061982", - "signature": pk.sign_digest( - hashlib.sha256(b"something else").digest(), - sigencode=ecdsa.util.sigencode_der - ).hex(), - "signed_by": "whosigned", - }) - self.assertFalse(valid_elem.is_valid(certifier)) - - -class TestHSMCertificateV2ElementSGXAttestationKey(TestCase): - def setUp(self): - self.elem = HSMCertificateV2ElementSGXAttestationKey({ - "name": "thename", - "message": "aabbcc", - "key": "ddeeff", - "auth_data": "112233", - "signature": "44556677", - "signed_by": "whosigned", - }) - - def test_props(self): - self.assertEqual("thename", self.elem.name) - self.assertEqual("whosigned", self.elem.signed_by) - self.assertEqual("aabbcc", self.elem.message) - self.assertEqual("ddeeff", self.elem.key) - self.assertEqual("112233", self.elem.auth_data) - self.assertEqual("44556677", self.elem.signature) - - def test_dict_ok(self): - self.assertEqual({ - "name": "thename", - "type": "sgx_attestation_key", - "message": "aabbcc", - "key": "ddeeff", - "auth_data": "112233", - "signature": "44556677", - "signed_by": "whosigned", - }, self.elem.to_dict()) - - def test_parse_identity(self): - source = TEST_CERTIFICATE["elements"][1] - elem = HSMCertificateV2Element.from_dict(source) - self.assertTrue(isinstance(elem, HSMCertificateV2ElementSGXAttestationKey)) - self.assertEqual(source, elem.to_dict()) - - def test_from_dict_invalid_message(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "attestation", - "type": "sgx_attestation_key", - "message": "not-hex", - "key": "eeff", - "auth_data": "112233", - "signature": "44556677", - "signed_by": "quoting_enclave" - }) - self.assertIn("Invalid message", str(e.exception)) - - def test_from_dict_invalid_key(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "attestation", - "type": "sgx_attestation_key", - "message": "aabbccdd", - "key": "not-hex", - "auth_data": "112233", - "signature": "44556677", - "signed_by": "quoting_enclave" - }) - self.assertIn("Invalid key", str(e.exception)) - - def test_from_dict_invalid_auth_data(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "attestation", - "type": "sgx_attestation_key", - "message": "aabbccdd", - "key": "eeff", - "auth_data": "not-hex", - "signature": "44556677", - "signed_by": "quoting_enclave" - }) - self.assertIn("Invalid auth data", str(e.exception)) - - def test_from_dict_invalid_signature(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "attestation", - "type": "sgx_attestation_key", - "message": "aabbccdd", - "key": "eeff", - "auth_data": "112233", - "signature": "not-hex", - "signed_by": "quoting_enclave" - }) - self.assertIn("Invalid signature", str(e.exception)) - - def test_get_value_notimplemented(self): - with self.assertRaises(NotImplementedError): - self.elem.get_value() - - -class TestHSMCertificateV2ElementX509(TestCase): - def setUp(self): - self.elem = HSMCertificateV2ElementX509({ - "name": "thename", - "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", - "signed_by": "whosigned", - }) - - def test_props(self): - self.assertEqual("thename", self.elem.name) - self.assertEqual("whosigned", self.elem.signed_by) - self.assertEqual("dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", self.elem.message) - - def test_dict_ok(self): - self.assertEqual({ - "name": "thename", - "type": "x509_pem", - "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", - "signed_by": "whosigned", - }, self.elem.to_dict()) - - def test_parse_identity(self): - source = TEST_CERTIFICATE["elements"][3] - elem = HSMCertificateV2Element.from_dict(source) - self.assertTrue(isinstance(elem, HSMCertificateV2ElementX509)) - self.assertEqual(source, elem.to_dict()) - - def test_from_dict_invalid_message(self): - with self.assertRaises(ValueError) as e: - HSMCertificateV2Element.from_dict({ - "name": "quoting_enclave", - "type": "x509_pem", - "message": "not-base-64", - "signed_by": "platform_ca" - }) - self.assertIn("Invalid message", str(e.exception)) - - def test_get_value_notimplemented(self): - with self.assertRaises(NotImplementedError): - self.elem.get_value() - - def test_from_pem(self): - self.assertEqual({ - "name": "thename", - "type": "x509_pem", - "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", - "signed_by": "whosigned", - }, HSMCertificateV2ElementX509.from_pem(""" - -----BEGIN CERTIFICATE----- - dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl - -----END CERTIFICATE----- - """, "thename", "whosigned").to_dict()) - - @patch("admin.certificate_v2.Path") - @patch("admin.certificate_v2.HSMCertificateV2ElementX509.from_pem") - def test_from_pemfile(self, from_pem, Path): - Path.return_value.read_text.return_value = "the pem contents" - from_pem.return_value = "the instance" - self.assertEqual("the instance", - HSMCertificateV2ElementX509.from_pemfile("a-file.pem", - "the name", - "who signed")) - Path.assert_called_with("a-file.pem") - from_pem.assert_called_with("the pem contents", - "the name", - "who signed") diff --git a/middleware/tests/admin/test_certificate_v2_element.py b/middleware/tests/admin/test_certificate_v2_element.py new file mode 100644 index 00000000..dcd717b5 --- /dev/null +++ b/middleware/tests/admin/test_certificate_v2_element.py @@ -0,0 +1,79 @@ +# 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. + +from unittest import TestCase +from admin.certificate_v2 import HSMCertificateV2Element + + +class TestHSMCertificateV2Element(TestCase): + def setUp(self): + class TestElement(HSMCertificateV2Element): + def __init__(self): + pass + + self.instance = TestElement() + + def test_from_dict_unknown_type(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "a-strange-name", + "type": "an-unknown-type", + "some": "other", + "random": "attributes", + }) + self.assertIn("a-strange-name", str(e.exception)) + + def test_from_dict_no_name(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "type": "sgx_quote", + "signed_by": "a-signer", + "some": "other", + "random": "attributes", + }) + self.assertIn("Missing name", str(e.exception)) + + def test_from_dict_no_signed_by(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "a name", + "type": "sgx_quote", + "some": "other", + "random": "attributes", + }) + self.assertIn("Missing certifier", str(e.exception)) + + def test_cant_instantiate(self): + with self.assertRaises(NotImplementedError): + HSMCertificateV2Element() + + def test_get_pubkey_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.get_pubkey() + + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.get_value() + + def test_is_valid_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.is_valid("a-certifier") diff --git a/middleware/tests/admin/test_certificate_v2_element_sgx_attestation_key.py b/middleware/tests/admin/test_certificate_v2_element_sgx_attestation_key.py new file mode 100644 index 00000000..e739cb9c --- /dev/null +++ b/middleware/tests/admin/test_certificate_v2_element_sgx_attestation_key.py @@ -0,0 +1,168 @@ +# 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 ecdsa +from unittest import TestCase +from unittest.mock import Mock +from admin.certificate_v2 import HSMCertificateV2Element, \ + HSMCertificateV2ElementSGXAttestationKey +from .test_certificate_v2_resources import TEST_CERTIFICATE + + +class TestHSMCertificateV2ElementSGXAttestationKey(TestCase): + def setUp(self): + self.source = TEST_CERTIFICATE["elements"][1] + self.elem = HSMCertificateV2ElementSGXAttestationKey(self.source) + valid_key = ecdsa.VerifyingKey.from_string( + bytes.fromhex("03a97b443365b192a412d01c5bb49f097d497a06ef1aae0ed2b454b74c" + "ff1ba7d9"), + ecdsa.NIST256p) + self.valid_certifier = Mock() + self.valid_certifier.get_pubkey.return_value = valid_key + + def test_props(self): + self.assertEqual("attestation", self.elem.name) + self.assertEqual("quoting_enclave", self.elem.signed_by) + self.assertEqual(bytes.fromhex( + "0e0e100fffff01000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000001500000000000000e70000000000000096b347a64e5a045e2736" + "9c26e6dcda51fd7c850e9b3a3a79e718f43261dee1e4000000000000000000000000000000" + "00000000000000000000000000000000008c4f5775d796503e96137f77c68a829a0056ac8d" + "ed70140b081b094490c57bff00000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000001000a" + "00000000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000001fe721d0322954821589237fd2" + "7efb8fef1acb3ecd6b0352c31271550fc70f94000000000000000000000000000000000000" + "0000000000000000000000000000"), self.elem.message.get_raw_data()) + self.assertEqual(bytes.fromhex( + "03a024cb34c90ea6a8f9f2181c9020cbcc7c073e69981733c8deed6f6c451822aa"), + self.elem.key.to_string("compressed")) + self.assertEqual( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + self.elem.auth_data) + self.assertEqual( + "304502201f14d532274c4385fc0019ca2a21e53e17143cb62377ca4fcdd97fa9fef8fb25022" + "10095d4ee272cf3c512e36779de67dc7814982f1160d981d138a32b265e928a0562", + self.elem.signature) + + def test_to_dict(self): + self.assertEqual(self.source, self.elem.to_dict()) + + def test_parse_identity(self): + elem = HSMCertificateV2Element.from_dict(self.source) + self.assertTrue(isinstance(elem, HSMCertificateV2ElementSGXAttestationKey)) + self.assertEqual(self.source, elem.to_dict()) + + def test_from_dict_invalid_message(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "message": "not-hex", + }) + self.assertIn("Invalid message", str(e.exception)) + + def test_from_dict_invalid_key(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "key": "not-hex", + }) + self.assertIn("Invalid key", str(e.exception)) + + def test_from_dict_invalid_auth_data(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "auth_data": "not-hex", + }) + self.assertIn("Invalid auth data", str(e.exception)) + + def test_from_dict_invalid_signature(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "signature": "not-hex", + }) + self.assertIn("Invalid signature", str(e.exception)) + + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_value() + + def test_is_valid_ok(self): + self.assertTrue(self.elem.is_valid(self.valid_certifier)) + + def test_is_valid_err_notthekey(self): + invalid_key = ecdsa.VerifyingKey.from_string( + bytes.fromhex("03986284e40eafc53a650547216176d4a227e1fa3a4473b76e48cfc442" + "efa004c4"), + ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = invalid_key + self.assertFalse(self.elem.is_valid(certifier)) + + def test_is_valid_err_message(self): + self.elem = HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "message": "1e0e100fffff010000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000001500000000000000e70000000000000096b" + "347a64e5a045e27369c26e6dcda51fd7c850e9b3a3a79e718f43261dee1e40000000000" + "0000000000000000000000000000000000000000000000000000008c4f5775d796503e9" + "6137f77c68a829a0056ac8ded70140b081b094490c57bff000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000001000a000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "0000000000001fe721d0322954821589237fd27efb8fef1acb3ecd6b0352c31271550fc" + "70f940000000000000000000000000000000000000000000000000000000000000000", + }) + self.assertFalse(self.elem.is_valid(self.valid_certifier)) + + def test_is_valid_err_message_invalid(self): + self.elem = HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "message": "aabbccdd", + }) + self.assertFalse(self.elem.is_valid(self.valid_certifier)) + + def test_is_valid_err_auth_data(self): + self.elem = HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "auth_data": "aabbccdd", + }) + self.assertFalse(self.elem.is_valid(self.valid_certifier)) + + def test_is_valid_err_key(self): + self.elem = HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "key": "03e2005bbf9db399bcba0b40d181b691f0d81287dbc1b6280bebd9247b" + "c0933f38", + }) + self.assertFalse(self.elem.is_valid(self.valid_certifier)) + + def test_is_valid_err_key_invalid(self): + self.elem = HSMCertificateV2ElementSGXAttestationKey({ + **self.source, + "key": "aabbccdd", + }) + self.assertFalse(self.elem.is_valid(self.valid_certifier)) diff --git a/middleware/tests/admin/test_certificate_v2_element_sgx_quote.py b/middleware/tests/admin/test_certificate_v2_element_sgx_quote.py new file mode 100644 index 00000000..5d7554ea --- /dev/null +++ b/middleware/tests/admin/test_certificate_v2_element_sgx_quote.py @@ -0,0 +1,170 @@ +# 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 ecdsa +import hashlib +from unittest import TestCase +from unittest.mock import Mock +from sgx.envelope import SgxQuote +from admin.certificate_v2 import HSMCertificateV2Element, \ + HSMCertificateV2ElementSGXQuote +from .test_certificate_v2_resources import TEST_CERTIFICATE + + +class TestHSMCertificateV2ElementSGXQuote(TestCase): + TEST_MESSAGE = \ + "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34d562f"\ + "4564fc182000000000e0e100fffff01000000000000000000010000000000000000000000000000"\ + "000000000000000000000000000000000005000000000000000700000000000000d32688d3c1f3d"\ + "fcc8b0b36eac7c89d49af331800bd56248044166fa6699442c10000000000000000000000000000"\ + "000000000000000000000000000000000000718c2f1a0efbd513e016fafd6cf62a624442f2d8370"\ + "8d4b33ab5a8d8c1cd4dd00000000000000000000000000000000000000000000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000640001000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ + "00000000000000000000000005d53b30e22f66979d36721e10ab7722557257a9ef8ba77ec7fe430"\ + "493c3542f90000000000000000000000000000000000000000000000000000000000000000" + + def setUp(self): + self.elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "ddeeff", + "signature": "112233", + "signed_by": "whosigned", + }) + + def test_props(self): + self.assertEqual("thename", self.elem.name) + self.assertEqual("whosigned", self.elem.signed_by) + self.assertIsInstance(self.elem.message, SgxQuote) + self.assertEqual(bytes.fromhex(self.TEST_MESSAGE), + self.elem.message.get_raw_data()) + self.assertEqual("ddeeff", self.elem.custom_data) + self.assertEqual("112233", self.elem.signature) + + def test_dict_ok(self): + self.assertEqual({ + "name": "thename", + "type": "sgx_quote", + "message": self.TEST_MESSAGE, + "custom_data": "ddeeff", + "signature": "112233", + "signed_by": "whosigned", + }, self.elem.to_dict()) + + def test_parse_identity(self): + source = TEST_CERTIFICATE["elements"][0] + elem = HSMCertificateV2Element.from_dict(source) + self.assertTrue(isinstance(elem, HSMCertificateV2ElementSGXQuote)) + self.assertEqual(source, elem.to_dict()) + + def test_from_dict_invalid_message(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quote", + "type": "sgx_quote", + "message": "not-hex", + "custom_data": "112233", + "signature": "445566778899", + "signed_by": "attestation" + }) + self.assertIn("Invalid message", str(e.exception)) + + def test_from_dict_invalid_custom_data(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quote", + "type": "sgx_quote", + "message": self.TEST_MESSAGE, + "custom_data": "not-hex", + "signature": "445566778899", + "signed_by": "attestation" + }) + self.assertIn("Invalid custom data", str(e.exception)) + + def test_from_dict_invalid_signature(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quote", + "type": "sgx_quote", + "message": self.TEST_MESSAGE, + "custom_data": "112233", + "signature": "not-hex", + "signed_by": "attestation" + }) + self.assertIn("Invalid signature", str(e.exception)) + + def test_get_pubkey_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_pubkey() + + def test_is_valid_ok(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "10061982", + "signature": pk.sign_digest( + hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertTrue(valid_elem.is_valid(certifier)) + + def test_is_valid_custom_data_mismatch(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "11061982", + "signature": pk.sign_digest( + hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertFalse(valid_elem.is_valid(certifier)) + + def test_is_valid_signature_mismatch(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "10061982", + "signature": pk.sign_digest( + hashlib.sha256(b"something else").digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertFalse(valid_elem.is_valid(certifier)) diff --git a/middleware/tests/admin/test_certificate_v2_element_x509.py b/middleware/tests/admin/test_certificate_v2_element_x509.py new file mode 100644 index 00000000..b5fd0c17 --- /dev/null +++ b/middleware/tests/admin/test_certificate_v2_element_x509.py @@ -0,0 +1,200 @@ +# 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. + +from unittest import TestCase +from unittest.mock import Mock, patch +from ecdsa import NIST256p +from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding +from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 +from admin.certificate_v2 import HSMCertificateV2Element, \ + HSMCertificateV2ElementX509 +from .test_certificate_v2_resources import TEST_CERTIFICATE + + +class TestHSMCertificateV2ElementX509(TestCase): + def setUp(self): + self.elem = HSMCertificateV2ElementX509({ + "name": "thename", + "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", + "signed_by": "whosigned", + }) + + def test_props(self): + self.assertEqual("thename", self.elem.name) + self.assertEqual("whosigned", self.elem.signed_by) + self.assertEqual("dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", self.elem.message) + + def test_dict_ok(self): + self.assertEqual({ + "name": "thename", + "type": "x509_pem", + "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", + "signed_by": "whosigned", + }, self.elem.to_dict()) + + def test_parse_identity(self): + source = TEST_CERTIFICATE["elements"][3] + elem = HSMCertificateV2Element.from_dict(source) + self.assertTrue(isinstance(elem, HSMCertificateV2ElementX509)) + self.assertEqual(source, elem.to_dict()) + + def test_from_dict_invalid_message(self): + with self.assertRaises(ValueError) as e: + HSMCertificateV2Element.from_dict({ + "name": "quoting_enclave", + "type": "x509_pem", + "message": "not-base-64", + "signed_by": "platform_ca" + }) + self.assertIn("Invalid message", str(e.exception)) + + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_value() + + def test_from_pem(self): + self.assertEqual({ + "name": "thename", + "type": "x509_pem", + "message": "dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl", + "signed_by": "whosigned", + }, HSMCertificateV2ElementX509.from_pem(""" + -----BEGIN CERTIFICATE----- + dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl + -----END CERTIFICATE----- + """, "thename", "whosigned").to_dict()) + + @patch("admin.certificate_v2.Path") + @patch("admin.certificate_v2.HSMCertificateV2ElementX509.from_pem") + def test_from_pemfile(self, from_pem, Path): + Path.return_value.read_text.return_value = "the pem contents" + from_pem.return_value = "the instance" + self.assertEqual("the instance", + HSMCertificateV2ElementX509.from_pemfile("a-file.pem", + "the name", + "who signed")) + Path.assert_called_with("a-file.pem") + from_pem.assert_called_with("the pem contents", + "the name", + "who signed") + + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_certificate(self, load_pem_x509_certificate): + load_pem_x509_certificate.return_value = "mock-certificate" + + self.assertEqual("mock-certificate", self.elem.certificate) + self.assertEqual("mock-certificate", self.elem.certificate) + + load_pem_x509_certificate.assert_called_with( + b"-----BEGIN CERTIFICATE-----" + b"dGhpcyBpcyBhbiBhc2NpaSBtZXNzYWdl" + b"-----END CERTIFICATE-----" + ) + self.assertEqual(1, load_pem_x509_certificate.call_count) + + def setup_is_valid_mocks(self, load_pem_x509_certificate, VerifyingKey): + self.pubkey = Mock() + self.pubkey.curve = SECP256R1() + self.pubkey.public_bytes.return_value = "the-public-bytes" + self.cert = Mock() + self.cert.public_key.return_value = self.pubkey + load_pem_x509_certificate.return_value = self.cert + VerifyingKey.from_string.return_value = "the-expected-pubkey" + + @patch("admin.certificate_v2.ecdsa.VerifyingKey") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_get_pubkey_ok(self, load_pem_x509_certificate, VerifyingKey): + self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + + self.assertEqual("the-expected-pubkey", self.elem.get_pubkey()) + self.pubkey.public_bytes.assert_called_with( + Encoding.X962, PublicFormat.CompressedPoint) + VerifyingKey.from_string.assert_called_with("the-public-bytes", NIST256p) + + @patch("admin.certificate_v2.ecdsa.VerifyingKey") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_get_pubkey_err_load_cert(self, load_pem_x509_certificate, VerifyingKey): + self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + load_pem_x509_certificate.side_effect = Exception("blah blah") + + with self.assertRaises(ValueError) as e: + self.elem.get_pubkey() + self.assertIn("gathering public key", str(e.exception)) + self.assertIn("blah blah", str(e.exception)) + self.pubkey.public_bytes.assert_not_called() + VerifyingKey.from_string.assert_not_called() + + @patch("admin.certificate_v2.ecdsa.VerifyingKey") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_get_pubkey_err_get_pub(self, load_pem_x509_certificate, VerifyingKey): + self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.cert.public_key.side_effect = Exception("blah blah") + + with self.assertRaises(ValueError) as e: + self.elem.get_pubkey() + self.assertIn("gathering public key", str(e.exception)) + self.assertIn("blah blah", str(e.exception)) + self.pubkey.public_bytes.assert_not_called() + VerifyingKey.from_string.assert_not_called() + + @patch("admin.certificate_v2.ecdsa.VerifyingKey") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_get_pubkey_err_pub_notnistp256(self, load_pem_x509_certificate, + VerifyingKey): + self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.pubkey.curve = "somethingelse" + + with self.assertRaises(ValueError) as e: + self.elem.get_pubkey() + self.assertIn("gathering public key", str(e.exception)) + self.assertIn("NIST P-256", str(e.exception)) + self.pubkey.public_bytes.assert_not_called() + VerifyingKey.from_string.assert_not_called() + + @patch("admin.certificate_v2.ecdsa.VerifyingKey") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_get_pubkey_err_public_bytes(self, load_pem_x509_certificate, VerifyingKey): + self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.pubkey.public_bytes.side_effect = Exception("blah blah") + + with self.assertRaises(ValueError) as e: + self.elem.get_pubkey() + self.assertIn("gathering public key", str(e.exception)) + self.assertIn("blah blah", str(e.exception)) + self.pubkey.public_bytes.assert_called_with( + Encoding.X962, PublicFormat.CompressedPoint) + VerifyingKey.from_string.assert_not_called() + + @patch("admin.certificate_v2.ecdsa.VerifyingKey") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_get_pubkey_err_ecdsafromstring(self, load_pem_x509_certificate, + VerifyingKey): + self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + VerifyingKey.from_string.side_effect = Exception("blah blah") + + with self.assertRaises(ValueError) as e: + self.elem.get_pubkey() + self.assertIn("gathering public key", str(e.exception)) + self.assertIn("blah blah", str(e.exception)) + self.pubkey.public_bytes.assert_called_with( + Encoding.X962, PublicFormat.CompressedPoint) + VerifyingKey.from_string.assert_called_with("the-public-bytes", NIST256p) From 92445fd6ab8b592a4feedba93d7f4ff0b3830957 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Wed, 8 Jan 2025 06:07:34 +1300 Subject: [PATCH 15/21] Certificate V2 x509_pem type element validation (#240) * Certificate V2 x509_pem type element validation - Implemented HSMCertificateV2ElementX509's is_valid method - Added root of trust element validation in SGX attestation verification - Added and updated unit tests * Changes as per PR review --- middleware/admin/certificate_v2.py | 36 +++++- middleware/admin/verify_sgx_attestation.py | 3 + .../admin/test_certificate_v2_element_x509.py | 107 ++++++++++++++++-- .../admin/test_verify_sgx_attestation.py | 54 +++++++-- 4 files changed, 182 insertions(+), 18 deletions(-) diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index c61faa3c..e0488d54 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -21,13 +21,14 @@ # SOFTWARE. import re -from pathlib import Path import base64 import ecdsa import hashlib +from datetime import datetime, UTC +from pathlib import Path from cryptography import x509 from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding -from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 +from cryptography.hazmat.primitives.asymmetric import ec from .certificate_v1 import HSMCertificate from .utils import is_nonempty_hex_string from sgx.envelope import SgxQuote, SgxReportBody @@ -254,13 +255,40 @@ def certificate(self): return self._certificate def is_valid(self, certifier): - return True + try: + # IMPORTANT: for now, we only allow verifying the validity of an + # HSMCertificateV2ElementX509 using another HSMCertificateV2ElementX509 + # instance as certifier. That way, we simplify the validation procedure + # and ensure maximum use of the underlying library's capabilities + # (cryptography) + if not isinstance(certifier, type(self)): + return False + + subject = self.certificate + issuer = certifier.certificate + now = datetime.now(UTC) + + # 1. Check validity period + if subject.not_valid_before_utc > now or subject.not_valid_after_utc < now: + return False + + # 2. Verify the signature + issuer.public_key().verify( + subject.signature, + subject.tbs_certificate_bytes, + ec.ECDSA(subject.signature_hash_algorithm) + ) + + return True + + except Exception: + return False def get_pubkey(self): try: public_key = self.certificate.public_key() - if not isinstance(public_key.curve, SECP256R1): + if not isinstance(public_key.curve, ec.SECP256R1): raise ValueError("Certificate does not have a NIST P-256 public key") public_bytes = public_key.public_bytes( diff --git a/middleware/admin/verify_sgx_attestation.py b/middleware/admin/verify_sgx_attestation.py index 1e8672fb..bb0f2903 100644 --- a/middleware/admin/verify_sgx_attestation.py +++ b/middleware/admin/verify_sgx_attestation.py @@ -53,6 +53,9 @@ def do_verify_attestation(options): info(f"Attempting to gather root authority from {root_authority}...") try: root_of_trust = get_root_of_trust(root_authority) + info("Attempting to validate self-signed root authority...") + if not root_of_trust.is_valid(root_of_trust): + raise ValueError("Failed to validate self-signed root of trust") except Exception as e: raise AdminError(f"Invalid root authority {root_authority}: {e}") info(f"Using {root_authority} as root authority") diff --git a/middleware/tests/admin/test_certificate_v2_element_x509.py b/middleware/tests/admin/test_certificate_v2_element_x509.py index b5fd0c17..f4f8f4dc 100644 --- a/middleware/tests/admin/test_certificate_v2_element_x509.py +++ b/middleware/tests/admin/test_certificate_v2_element_x509.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from datetime import datetime, timedelta, UTC from unittest import TestCase from unittest.mock import Mock, patch from ecdsa import NIST256p @@ -111,7 +112,7 @@ def test_certificate(self, load_pem_x509_certificate): ) self.assertEqual(1, load_pem_x509_certificate.call_count) - def setup_is_valid_mocks(self, load_pem_x509_certificate, VerifyingKey): + def setup_pubkey_mocks(self, load_pem_x509_certificate, VerifyingKey): self.pubkey = Mock() self.pubkey.curve = SECP256R1() self.pubkey.public_bytes.return_value = "the-public-bytes" @@ -123,7 +124,7 @@ def setup_is_valid_mocks(self, load_pem_x509_certificate, VerifyingKey): @patch("admin.certificate_v2.ecdsa.VerifyingKey") @patch("admin.certificate_v2.x509.load_pem_x509_certificate") def test_get_pubkey_ok(self, load_pem_x509_certificate, VerifyingKey): - self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) self.assertEqual("the-expected-pubkey", self.elem.get_pubkey()) self.pubkey.public_bytes.assert_called_with( @@ -133,7 +134,7 @@ def test_get_pubkey_ok(self, load_pem_x509_certificate, VerifyingKey): @patch("admin.certificate_v2.ecdsa.VerifyingKey") @patch("admin.certificate_v2.x509.load_pem_x509_certificate") def test_get_pubkey_err_load_cert(self, load_pem_x509_certificate, VerifyingKey): - self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) load_pem_x509_certificate.side_effect = Exception("blah blah") with self.assertRaises(ValueError) as e: @@ -146,7 +147,7 @@ def test_get_pubkey_err_load_cert(self, load_pem_x509_certificate, VerifyingKey) @patch("admin.certificate_v2.ecdsa.VerifyingKey") @patch("admin.certificate_v2.x509.load_pem_x509_certificate") def test_get_pubkey_err_get_pub(self, load_pem_x509_certificate, VerifyingKey): - self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) self.cert.public_key.side_effect = Exception("blah blah") with self.assertRaises(ValueError) as e: @@ -160,7 +161,7 @@ def test_get_pubkey_err_get_pub(self, load_pem_x509_certificate, VerifyingKey): @patch("admin.certificate_v2.x509.load_pem_x509_certificate") def test_get_pubkey_err_pub_notnistp256(self, load_pem_x509_certificate, VerifyingKey): - self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) self.pubkey.curve = "somethingelse" with self.assertRaises(ValueError) as e: @@ -173,7 +174,7 @@ def test_get_pubkey_err_pub_notnistp256(self, load_pem_x509_certificate, @patch("admin.certificate_v2.ecdsa.VerifyingKey") @patch("admin.certificate_v2.x509.load_pem_x509_certificate") def test_get_pubkey_err_public_bytes(self, load_pem_x509_certificate, VerifyingKey): - self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) self.pubkey.public_bytes.side_effect = Exception("blah blah") with self.assertRaises(ValueError) as e: @@ -188,7 +189,7 @@ def test_get_pubkey_err_public_bytes(self, load_pem_x509_certificate, VerifyingK @patch("admin.certificate_v2.x509.load_pem_x509_certificate") def test_get_pubkey_err_ecdsafromstring(self, load_pem_x509_certificate, VerifyingKey): - self.setup_is_valid_mocks(load_pem_x509_certificate, VerifyingKey) + self.setup_pubkey_mocks(load_pem_x509_certificate, VerifyingKey) VerifyingKey.from_string.side_effect = Exception("blah blah") with self.assertRaises(ValueError) as e: @@ -198,3 +199,95 @@ def test_get_pubkey_err_ecdsafromstring(self, load_pem_x509_certificate, self.pubkey.public_bytes.assert_called_with( Encoding.X962, PublicFormat.CompressedPoint) VerifyingKey.from_string.assert_called_with("the-public-bytes", NIST256p) + + def setup_is_valid_mocks(self, load_pem_x509_certificate, ec): + self.certifier = HSMCertificateV2ElementX509({ + "name": "mock-certifier", + "signed_by": "someone-else", + "message": "Y2VydGlmaWVy" + }) + + self.mock_certifier = Mock() + self.mock_elem = Mock() + + def load_mock(data): + if b"Y2VydGlmaWVy" in data: + return self.mock_certifier + return self.mock_elem + + load_pem_x509_certificate.side_effect = load_mock + + self.now = datetime.now(UTC) + one_week = timedelta(weeks=1) + self.mock_elem.not_valid_before_utc = self.now - one_week + self.mock_elem.not_valid_after_utc = self.now + one_week + self.mock_certifier_pk = Mock() + self.mock_certifier.public_key.return_value = self.mock_certifier_pk + self.mock_elem.signature = "the-signature" + self.mock_elem.tbs_certificate_bytes = "the-fingerprint" + self.mock_elem.signature_hash_algorithm = "the-signature-hash-algo" + ec.ECDSA.return_value = "the-ecdsa-algo" + + @patch("admin.certificate_v2.ec") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_is_valid_ok(self, load_pem_x509_certificate, ec): + self.setup_is_valid_mocks(load_pem_x509_certificate, ec) + + self.assertTrue(self.elem.is_valid(self.certifier)) + + self.mock_certifier_pk.verify.assert_called_with( + "the-signature", + "the-fingerprint", + "the-ecdsa-algo" + ) + ec.ECDSA.assert_called_with("the-signature-hash-algo") + + @patch("admin.certificate_v2.ec") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_is_valid_before_in_future(self, load_pem_x509_certificate, ec): + self.setup_is_valid_mocks(load_pem_x509_certificate, ec) + self.mock_elem.not_valid_before_utc = self.now + \ + timedelta(minutes=1) + + self.assertFalse(self.elem.is_valid(self.certifier)) + + self.mock_certifier_pk.verify.assert_not_called() + ec.ECDSA.assert_not_called() + + @patch("admin.certificate_v2.ec") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_is_valid_after_in_past(self, load_pem_x509_certificate, ec): + self.setup_is_valid_mocks(load_pem_x509_certificate, ec) + self.mock_elem.not_valid_after_utc = self.now - \ + timedelta(minutes=1) + + self.assertFalse(self.elem.is_valid(self.certifier)) + + self.mock_certifier_pk.verify.assert_not_called() + ec.ECDSA.assert_not_called() + + @patch("admin.certificate_v2.ec") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_is_valid_signature_invalid(self, load_pem_x509_certificate, ec): + self.setup_is_valid_mocks(load_pem_x509_certificate, ec) + self.mock_certifier_pk.verify.side_effect = RuntimeError("wrong signature") + + self.assertFalse(self.elem.is_valid(self.certifier)) + + self.mock_certifier_pk.verify.assert_called_with( + "the-signature", + "the-fingerprint", + "the-ecdsa-algo" + ) + ec.ECDSA.assert_called_with("the-signature-hash-algo") + + @patch("admin.certificate_v2.ec") + @patch("admin.certificate_v2.x509.load_pem_x509_certificate") + def test_is_valid_x509_error(self, load_pem_x509_certificate, ec): + self.setup_is_valid_mocks(load_pem_x509_certificate, ec) + load_pem_x509_certificate.side_effect = ValueError("a random error") + + self.assertFalse(self.elem.is_valid(self.certifier)) + + self.mock_certifier_pk.verify.assert_not_called() + ec.ECDSA.assert_not_called() diff --git a/middleware/tests/admin/test_verify_sgx_attestation.py b/middleware/tests/admin/test_verify_sgx_attestation.py index 5bacadf5..cf980d9b 100644 --- a/middleware/tests/admin/test_verify_sgx_attestation.py +++ b/middleware/tests/admin/test_verify_sgx_attestation.py @@ -94,7 +94,9 @@ def setUp(self): def configure_mocks(self, get_root_of_trust, load_pubkeys, HSMCertificate, head): - get_root_of_trust.return_value = "the-root-of-trust" + self.root_of_trust = Mock() + self.root_of_trust.is_valid.return_value = True + get_root_of_trust.return_value = self.root_of_trust load_pubkeys.return_value = self.public_keys self.mock_certificate = Mock() self.mock_certificate.validate_and_get_values.return_value = self.validate_result @@ -116,10 +118,11 @@ def test_verify_attestation(self, get_root_of_trust, load_pubkeys, get_root_of_trust.assert_called_with(custom_root) else: get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) self.mock_certificate.validate_and_get_values \ - .assert_called_with("the-root-of-trust") + .assert_called_with(self.root_of_trust) head.assert_called_with([ "powHSM verified with public keys:" ] + self.expected_pubkeys_output + [ @@ -135,6 +138,36 @@ def test_verify_attestation(self, get_root_of_trust, load_pubkeys, "Timestamp: 205", ], fill="-") + def test_verify_attestation_err_get_root(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + get_root_of_trust.side_effect = ValueError("root of trust error") + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("root of trust error", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_not_called() + load_pubkeys.assert_not_called() + HSMCertificate.from_jsonfile.assert_not_called() + self.mock_certificate.validate_and_get_values.assert_not_called() + + def test_verify_attestation_err_root_invalid(self, get_root_of_trust, load_pubkeys, + HSMCertificate, head, _): + self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) + self.root_of_trust.is_valid.return_value = False + + with self.assertRaises(AdminError) as e: + do_verify_attestation(self.options) + self.assertIn("self-signed root of trust", str(e.exception)) + + get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) + load_pubkeys.assert_not_called() + HSMCertificate.from_jsonfile.assert_not_called() + self.mock_certificate.validate_and_get_values.assert_not_called() + def test_verify_attestation_err_load_pubkeys(self, get_root_of_trust, load_pubkeys, HSMCertificate, head, _): self.configure_mocks(get_root_of_trust, load_pubkeys, HSMCertificate, head) @@ -145,6 +178,7 @@ def test_verify_attestation_err_load_pubkeys(self, get_root_of_trust, load_pubke self.assertIn("pubkeys error", str(e.exception)) get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_not_called() self.mock_certificate.validate_and_get_values.assert_not_called() @@ -159,6 +193,7 @@ def test_verify_attestation_err_load_cert(self, get_root_of_trust, load_pubkeys, self.assertIn("load cert error", str(e.exception)) get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) self.mock_certificate.validate_and_get_values.assert_not_called() @@ -173,10 +208,11 @@ def test_verify_attestation_validation_noquote(self, get_root_of_trust, load_pub self.assertIn("does not contain", str(e.exception)) get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) self.mock_certificate.validate_and_get_values \ - .assert_called_with("the-root-of-trust") + .assert_called_with(self.root_of_trust) def test_verify_attestation_validation_failed(self, get_root_of_trust, load_pubkeys, HSMCertificate, head, _): @@ -190,10 +226,11 @@ def test_verify_attestation_validation_failed(self, get_root_of_trust, load_pubk self.assertIn("validation error", str(e.exception)) get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) self.mock_certificate.validate_and_get_values \ - .assert_called_with("the-root-of-trust") + .assert_called_with(self.root_of_trust) def test_verify_attestation_invalid_header(self, get_root_of_trust, load_pubkeys, HSMCertificate, head, _): @@ -205,10 +242,11 @@ def test_verify_attestation_invalid_header(self, get_root_of_trust, load_pubkeys self.assertIn("message header", str(e.exception)) get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) self.mock_certificate.validate_and_get_values \ - .assert_called_with("the-root-of-trust") + .assert_called_with(self.root_of_trust) def test_verify_attestation_invalid_message(self, get_root_of_trust, load_pubkeys, HSMCertificate, head, _): @@ -220,10 +258,11 @@ def test_verify_attestation_invalid_message(self, get_root_of_trust, load_pubkey self.assertIn("parsing", str(e.exception)) get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) self.mock_certificate.validate_and_get_values \ - .assert_called_with("the-root-of-trust") + .assert_called_with(self.root_of_trust) def test_verify_attestation_pkh_mismatch(self, get_root_of_trust, load_pubkeys, HSMCertificate, head, _): @@ -235,7 +274,8 @@ def test_verify_attestation_pkh_mismatch(self, get_root_of_trust, load_pubkeys, self.assertIn("hash mismatch", str(e.exception)) get_root_of_trust.assert_called_with(DEFAULT_ROOT_AUTHORITY) + self.root_of_trust.is_valid.assert_called_with(self.root_of_trust) load_pubkeys.assert_called_with(self.pubkeys_path) HSMCertificate.from_jsonfile.assert_called_with(self.certification_path) self.mock_certificate.validate_and_get_values \ - .assert_called_with("the-root-of-trust") + .assert_called_with(self.root_of_trust) From ccb345a288197159ce397d21c5ef34b10c50975c Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Wed, 8 Jan 2025 10:18:28 +1300 Subject: [PATCH 16/21] SGX onboarding attestation gathering and verification (#242) - Updated setup script to gather and verify attestation after onboarding is completed - Added runtime attestation dependencies to the SGX distribution Dockerfile --- dist/sgx/Dockerfile | 8 +++++++- dist/sgx/scripts/setup | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/dist/sgx/Dockerfile b/dist/sgx/Dockerfile index 1805e567..dbb318ce 100644 --- a/dist/sgx/Dockerfile +++ b/dist/sgx/Dockerfile @@ -12,6 +12,12 @@ RUN curl -L -o libssl1.1.deb https://ftp.debian.org/debian/pool/main/o/openssl/ # Install SGX runtime dependencies RUN echo 'deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu focal main' | tee /etc/apt/sources.list.d/intel-sgx.list && \ + echo 'deb [arch=amd64] http://azure.archive.ubuntu.com/ubuntu/ focal main restricted' | tee -a /etc/apt/sources.list.d/intel-sgx.list && \ + echo 'deb [arch=amd64] https://packages.microsoft.com/ubuntu/20.04/prod focal main' | tee -a /etc/apt/sources.list.d/intel-sgx.list && \ + gpg --keyserver keyserver.ubuntu.com --recv-keys 871920D1991BC93C 3B4FE6ACC0B21F32 EB3E94ADBE1229CF && \ + gpg --export --armor 871920D1991BC93C | apt-key add - && \ + gpg --export --armor 3B4FE6ACC0B21F32 | apt-key add - && \ + gpg --export --armor EB3E94ADBE1229CF | apt-key add - && \ curl -fsSL https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | apt-key add - && \ apt-get update && \ - apt-get install -y libsgx-enclave-common + apt-get install -y libsgx-enclave-common libsgx-quote-ex libsgx-dcap-ql az-dcap-client diff --git a/dist/sgx/scripts/setup b/dist/sgx/scripts/setup index 2170614d..3e816f0b 100755 --- a/dist/sgx/scripts/setup +++ b/dist/sgx/scripts/setup @@ -34,6 +34,7 @@ PIN_FILE="$ROOT_DIR/pin.txt" EXPORT_DIR="$ROOT_DIR/export" PUBLIC_KEY_FILE="$EXPORT_DIR/public-keys.txt" PUBLIC_KEY_FILE_JSON="$EXPORT_DIR/public-keys.json" +ATTESTATION_FILE="$EXPORT_DIR/attestation.json" # HSM scripts directory SCRIPTS_DIR=$ROOT_DIR/scripts @@ -139,7 +140,17 @@ function onboard() { } function keys() { - $ADMIN pubkeys -o $PUBLIC_KEY_FILE + $ADMIN pubkeys -uo $PUBLIC_KEY_FILE + error +} + +function attestation() { + $ADMIN attestation -o $ATTESTATION_FILE + error +} + +function verify_attestation() { + $ADMIN verify_attestation -t $ATTESTATION_FILE -b $PUBLIC_KEY_FILE_JSON error } @@ -161,10 +172,16 @@ echo -e "\e[1;33mOnboarding the powHSM... \e[0m" onboard echo -e "\e[1;33mOnboarding complete.\e[0m" echo -echo -e "\e[1;32mGathering public keys\e[0m" +echo -e "\e[1;32mGathering attestation\e[0m" createOutputDir +attestation +echo +echo -e "\e[1;32mGathering public keys\e[0m" keys echo +echo -e "\e[1;32mVerifying attestation\e[0m" +verify_attestation +echo echo -e "\e[1;32mStopping the powHSM...\e[0m" stopPowHsm cleanBinaries From 69f1a5023b8b10fce09cf31626939376487af4aa Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Thu, 9 Jan 2025 04:31:18 +1300 Subject: [PATCH 17/21] SGX heartbeat disabling (#243) - Throwing ERR_INS_NOT_SUPPORTED for the heartbeat operation in SGX only - Added test case to system module unit tests --- firmware/src/sgx/src/trusted/system.c | 4 +++ firmware/src/sgx/test/system/test_system.c | 29 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/firmware/src/sgx/src/trusted/system.c b/firmware/src/sgx/src/trusted/system.c index 09fe938c..02545dc6 100644 --- a/firmware/src/sgx/src/trusted/system.c +++ b/firmware/src/sgx/src/trusted/system.c @@ -147,6 +147,10 @@ static external_processor_result_t system_do_process_apdu(unsigned int rx) { REQUIRE_UNLOCKED(); result.tx = do_change_password(rx); break; + case INS_HEARTBEAT: + // For now, we don't support heartbeat in SGX + THROW(ERR_INS_NOT_SUPPORTED); + break; default: result.handled = false; } diff --git a/firmware/src/sgx/test/system/test_system.c b/firmware/src/sgx/test/system/test_system.c index d49fdf3d..8ee70584 100644 --- a/firmware/src/sgx/test/system/test_system.c +++ b/firmware/src/sgx/test/system/test_system.c @@ -31,6 +31,7 @@ #include "hal/communication.h" #include "hal/exceptions.h" #include "hal/log.h" +#include "apdu.h" #include "hsm.h" #include "system.h" @@ -944,6 +945,33 @@ void test_retries_cmd_handled() { ASSERT_APDU("\x80\xA2\x03"); } +void test_heartbeat_cmd_throws_unsupported() { + setup(); + printf("Test heartbeat command throws unsupported instruction...\n"); + + system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer)); + unsigned int rx = 0; + SEED_SET_AVAILABLE(true); + ACCESS_UNLOCK(); + SET_APDU("\x80\x60\x00", rx); // SGX_ONBOARD + BEGIN_TRY { + TRY { + system_process_apdu(rx); + // system_process_apdu should throw ERR_INS_NOT_SUPPORTED + ASSERT_FAIL(); + } + CATCH_OTHER(e) { + assert(e == ERR_INS_NOT_SUPPORTED); + } + FINALLY { + ASSERT_NOT_HANDLED(); + teardown(); + return; + } + } + END_TRY; +} + void test_invalid_cmd_not_handled() { setup(); printf("Test invalid command is ignored...\n"); @@ -986,6 +1014,7 @@ int main() { test_echo_cmd_handled(); test_is_locked_cmd_handled(); test_retries_cmd_handled(); + test_heartbeat_cmd_throws_unsupported(); test_invalid_cmd_not_handled(); return 0; From 2b181c9e949c488b47bb281705586d40a3402640 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Tue, 14 Jan 2025 07:54:27 +1300 Subject: [PATCH 18/21] Fix middleware docker image build (#253) --- docker/mware/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/mware/Dockerfile b/docker/mware/Dockerfile index b9bad088..8c7055a2 100644 --- a/docker/mware/Dockerfile +++ b/docker/mware/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update && \ # Python package prerequisites RUN apt-get install -y \ libsecp256k1-dev=0.2.0-2 \ - libudev-dev=252.31-1~deb12u1 \ + libudev-dev=252.33-1~deb12u1 \ libusb-1.0-0-dev=2:1.0.26-1 \ libffi-dev=3.4.4-1 \ libjpeg-dev=1:2.1.5-2 From 3a186bb719ccf1ad2bdcf7bd51e128fec510ead5 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Wed, 15 Jan 2025 01:50:58 +1300 Subject: [PATCH 19/21] SGX attestation documentation (#254) - Updated main README - Added SGX stuff to attestation documentation - Added reproducible builds information to build README --- README.md | 2 +- docs/attestation.md | 188 +++++++++++++++++++++++++++++++++------ firmware/build/README.md | 4 + 3 files changed, 168 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 7c54a95f..9f19d322 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ powHSM is a solution designed specifically for the [RSK network](https://www.rsk 1. The first implementation consists of a pair of applications for the [Ledger Nano S](https://shop.ledger.com/products/ledger-nano-s), namely a UI and a Signer, and it strongly depends on the device's security features to implement the aforementioned safekeeping. This implementation requires a physical Ledger Nano S device and a self-managed physical standalone server. 2. The second implementation consists of both a host and an enclave binary targetting the Intel SGX architecture. Just as the Ledger Nano S implementation, it strongly depends on the Intel SGX security features in order to keep the private keys safe. This implementation can run both on standalone SGX-enabled servers as well as on SGX-enabled cloud computing providers (e.g., Microsoft Azure). -Each powPeg member runs an individual physical device or SGX enclave on which a transparent installation and onboarding process is carried. Amongst other things, this process safely generates the root key, that either never leaves the device (Ledger) or can only ever be decrypted by the enclave (SGX). There is an [attestation process](./docs/attestation.md) that serves the purpose of testifying and guaranteeing this key generation process, and ultimately the fact that the key is only ever known to the device (attestation is currently only supported on the Ledger implementation). +Each powPeg member runs an individual physical device or SGX enclave on which a transparent installation and onboarding process is carried. Amongst other things, this process safely generates the root key, that either never leaves the device (Ledger) or can only ever be decrypted by the enclave (SGX). There is an [attestation process](./docs/attestation.md) that serves the purpose of testifying and guaranteeing this key generation process, and ultimately the fact that the key is only ever known to the physical device or SGX enclave. After onboarding, each powHSM runs either on its host (SGX) or is physically connected to it (Ledger), and interacts with its corresponding powPeg node by means of a middleware layer that exposes a [high-level protocol](./docs/protocol.md) for its operation. diff --git a/docs/attestation.md b/docs/attestation.md index 530aa713..0ff97a8e 100644 --- a/docs/attestation.md +++ b/docs/attestation.md @@ -1,18 +1,19 @@ # powHSM attestation -## Foreword +## Abstract -Currently, attestation is a feature supported only in the Ledger version of powHSM. An attestation implementation for the SGX version of powHSM is currently under development. Therefore, all the information contained herein must be interpreted as applying exclusively to the Ledger version of powHSM. +This document describes the mechanisms through which a powHSM installation can prove to an end user that either: -## Abstract +- It is actually installed an authentic physical Ledger Nano S device with specific UI and Signer versions, along with its currently authorized signer version and generated public keys; or +- It is running in an authentic Intel SGX environment, within an SGX enclave with a specific codebase, along with its safely generated and stored public keys. -This document describes the mechanisms through which a powHSM installation can prove to an end user that it is actually installed on an authentic physical Ledger device with specific UI and Signer versions, along with its currently authorized signer version and generated public keys. +## Attestation for the Ledger-based powHSM -## Preliminaries, native support and assumptions +### Preliminaries, native support and assumptions Each Ledger device currently used to run powHSM on, namely Ledger Nano S, ships with a mechanism to prove its authenticity and that also enables and leverages some basic additional support for user application attestation. For powHSM attestation we make extensive use of these mechanisms, assuming it is robust enough for our purpose. -## Device key and authenticity +### Device key and authenticity The mechanism used by Ledger Nano S devices to prove their authenticity can be better understood from [the ledger documentation](https://developers.ledger.com/docs/nano-app/bolos-features/#attestation): @@ -20,7 +21,7 @@ _"When all Ledger devices are provisioned in the factory, they first generate a We use the device public key and issuer certificate as the basis for the powHSM attestation mechanism. -## Application attestation and powHSM +### Application attestation and powHSM Ledger Nano S user applications can make indirect use of the aforementioned device keypair to provide attestation mechanisms. This can be better understood from [the ledger documentation](https://developers.ledger.com/docs/nano-app/bolos-features/#attestation): @@ -32,19 +33,19 @@ _"The attestation keys are not accessible to apps directly, instead BOLOS provid For powHSM, we use Endorsement Scheme Two, which provides a primitive to _"Sign a message using a private key derived from the attestation private key and the hash of the running application"_. In this way, installed applications can endorse specific messages, and that endorsement constitutes _proof_ of those messages being generated on that specific running code on an authentic Ledger Nano S. This is the basis for the powHSM attestation. -## Attestation goal +### Attestation goal -The main goal of the powHSM attestation mechanism is enabling the end user(s) to have proof of a specific powHSM with a given UI and Signer running on an authentic Ledger Nano S with a specific authorized signer version and having control over a given set of generated public keys. Given the constraints specifically implemented on the powHSM UI (more on this later), proof of the aforementioned would also guarantee that the holder of the powHSM device will not ever be able to alter the installed UI; and that upgrades for the Signer application will need the explicit authorization of a minimum number of predefined authorizers (currently hardcoded within the UI, and decided at compile time). Attempts to bypass these restrictions would result in the keypairs being lost forever. +The main goal of the Ledger-based powHSM attestation mechanism is enabling the end user(s) to have proof of a specific powHSM with a given UI and Signer running on an authentic Ledger Nano S with a specific authorized signer version and having control over a given set of generated public keys. Given the constraints specifically implemented on the powHSM UI (more on this later), proof of the aforementioned would also guarantee that the holder of the powHSM device will not ever be able to alter the installed UI; and that upgrades for the Signer application will need the explicit authorization of a minimum number of predefined authorizers (currently hardcoded within the UI, and decided at compile time). Attempts to bypass these restrictions would result in the keypairs being lost forever. -## Attestation gathering +### Attestation gathering -The attestation gathering process is actually a three step process: first, the attestation keypair is setup; second, the UI provides attestation for itself; last, the Signer provides an attestation for itself. Together, these three pieces form the powHSM attestation. +The attestation gathering process is actually a three step process: first, the attestation keypair is setup; second, the UI provides attestation for itself; last, the Signer provides an attestation for itself. Together, these three pieces form the powHSM attestation. Intermediate software layers unify these pieces into a user-friendly format that can be used for transmission and verification. -### Attestation keypair setup +#### Attestation keypair setup The attestation keypair setup takes place right after the onboarding is complete (any attestation keypairs generated before that are wiped). In this part of the process, also known as endorsement setup, the device generates a new keypair, which will be known as the attestation keypair. Then, it signs its public key with its device key, and then outputs the attestation public key, the aforementioned signature and the issuer's certificate of the device's keypair. This two-step certification chain can be used to prove that the generated attestation keypair was generated in an authentic ledger device and is under its control. It is important to mention that the _endorsement scheme number two_ is used for the attestation setup. This then implies that applications using the attestation key to sign messages actually use a derived key obtained from this key plus the running application hash. Therefore, a valid signature under this scheme is also proof of it being generated from a specific application. -### UI Attestation +#### UI Attestation Before diving into the UI attestation, it is important to recall a few relevant UI features: @@ -64,23 +65,52 @@ To generate the attestation, the UI uses the configured attestation scheme to si As a consequence of the aforementioned features, this message guarantees that the device is running a specific version of the UI with a specific seed and authorized signer version, and also that this cannot be changed without wiping the device, therefore losing the keys forever. The RSK best block hash also consitutes proof of a minimum date/time on which the attestation was generated. -### Signer attestation +#### Signer attestation + +To generate the attestation, the Signer uses the configured attestation scheme to sign a message that guarantees that the device is running a specific version of the Signer and that those keys are in control of the Ledger device. Additional fields aid in auditing a device's state at the time the attestation is gathered (e.g., for firmware updates). For details on the specific message signed, refer to the [powHSM attestation contents](#powhsm-attestation-contents) section. + +## Attestation for the Intel SGX-based powHSM + +### Preliminaries, native support and assumptions + +The Intel Software Guard Extensions (SGX) architecture features an advanced mechanism that allows a combination of hardware and software to gain a remote party's trust. This mechanism, known as remote attestation, gives the relying party the ability to check that the intended software is +securely running within an enclave on a system with Intel SGX enabled. For the Intel SGX-based powHSM attestation we make use of remote attestation, assuming it is robust enough for our purpose. In particular, we use ECDSA-based attestation using Intel SGX Data Center Attestation Primitives (DCAP), explained below. + +### Local and remote attestation + +Local attestation is a native Intel SGX process that can be by used by an enclave to verify the integrity and authenticity of another enclave running on the same physical platform. It enables the "verifier" enclave to confirm the identity, code, and state of the target enclave by exchanging a secure report, natively and securely generated using Intel SGX primitives. This report can additionally include arbitrary information generated by the target enclave, whose source the verifier enclave can deem trustworthy. + +Remote attestation extends local attestation to enable trust verification between an SGX enclave and a verifier outside the platform. In Intel SGX DCAP remote attestation, the target enclave generates a local attestation report, which is then sent to a specialized system enclave that transforms the local report into a "quote" by signing it with a platform-specific attestation key. This quote, along with a certificate that chains back to Intel's root of trust, is sent to the remote verifier. The verifier uses the certificate chain to validate the quote and, by extension, the enclave’s identity and platform security, enabling the remote party to trust the enclave. + +### Attestation goal + +The main goal of the SGX-based powHSM attestation mechanism is enabling the end user(s) to have proof of a powHSM with a given trusted codebase running within an authentic Intel SGX enclave and having control over a given set of generated public keys. Given the constraints specifically implemented on the powHSM enclave business layer alongside the specific primitives leveraged to encrypt/decrypt secrets within the SGX enclave (namely, the use of the enclave identity for the encryption key derivation functions), it is guaranteed that all enclave secrets (and, in particular, the master seed) will only ever be known to the powHSM enclave. Any attempts to modify the code (even having access to the enclave signer private key) will result in a different enclave identity and, thus, in an invalid set of derived keys that will make it impossible for the modified enclave to access the original enclave's secrets. + +### Attestation gathering + +As opposed to what happens with the Ledger-based powHSM, the attestation gathering process for the SGX-based powHSM is straightforward: upon request, the powHSM enclave produces a quote. This quote is, in itself, the entire attestation, but is then transformed by intermediate software layers into a user-friendly format that can be used for transmission and verification. + +## powHSM attestation contents -To generate the attestation, the Signer uses the configured attestation scheme to sign a message generated by the concatenation of: +Under both Ledger and SGX, the powHSM business layer includes an arbitrary message that is part of the final attestation produced, and that can also be verified and trusted by the interested parties. This message is generated by the concatenation of: - A predefined header (`POWHSM:5.4::`). -- A 3-byte platform identifier, which for Ledger is exactly the ASCII characters `led`. -- A 32 byte user-defined value. By default, the attestation generation client supplies the latest RSK block hash as this value, so it can then be used as a minimum timestamp reference for the attestation generation. +- A 3-byte platform identifier, which for Ledger and SGX are exactly the ASCII characters `led` or `sgx`, respectively. +- A 32 byte user-defined value, given by the requesting party. By default, the attestation generation client supplies the latest RSK block hash as this value, so it can then be used as a minimum timestamp reference for the attestation generation. - A 32 byte value that is generated by computing the `sha256sum` of the concatenation of the authorized public keys (see the [protocol](./protocol.md) for details on this) lexicographically ordered by their UTF-encoded derivation path. -- A 32 byte value denoting the device's current known best block hash for the Rootstock network. +- A 32 byte value denoting the powHSM's current known best block hash for the Rootstock network. - An 8 byte value denoting the leading bytes of the latest authorised signed Bitcoin transaction hash. -- An 8 byte value denoting a big-endian unix timestamp. For Ledger, this is always zero. +- An 8 byte value denoting a big-endian unix timestamp. For both Ledger and SGX, this is currently always zero. + +This message guarantees that the device is running a specific powHSM version and that the keys are in control of the Ledger device or SGX enclave. The additional fields aid in auditing a powHSM's state at the time the attestation is gathered. -This message guarantees that the device is running a specific version of the Signer and that those keys are in control of the ledger device. The additional fields aid in auditing a device's state at the time the attestation is gathered (e.g., for firmware updates). +## Attestation file formats -## Attestation file format +The output of the attestation process is a JSON file with a proprietary structure that allows for the validation of each of the attestation components all the way to the root of trust (Ledger or Intel, depending on the platform used). Currently, there's two versions of the attestation file used: version one is used in the Ledger-based implementation, and version two is used for the Intel SGX-based implementation. In the future, the idea is unifying everything into a single version in order to simplify the generation and verification process both from a software implementation and end-user perspective. -The output of the attestation process is a JSON file with a proprietary structure that allows for the validation of each of the attestation components (UI and Signer) all the way to the issuer's public key (Ledger), and also validating the generated attestation keypair as a byproduct. A sample attestation file is shown below: +### Attestation version one + +A sample attestation version one file is shown below: ```json { @@ -151,9 +181,77 @@ def extract(element): The validation process _for each of the targets_ is fairly straightforward, and can even be done manually with the aid of basic ECDSA and hashing tools: walk the element chain "upwards" until the element signed by `root` is found. Then start by validating that element's signature against the root public key and extracting that element's public key. Repeat the process walking the chain "downwards" until the target is reached. Fail if at any point an element's signature is invalid. Otherwise the target is valid and its value can be extracted from its `message` field and interpreted accordingly (in the case of the `ui` element, the user-defined value, public key and custom certification authority; in the case of the `signer`, the hash of the authorized public keys). +### Attestation version two + +A sample attestation version two file is shown below: + +```json +{ + "version": 2, + "targets": [ + "quote" + ], + "elements": [ + { + "name": "quote", + "type": "sgx_quote", + "message": "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34d562f4564fc182000000000e0e100fffff01000000000000000000010000000000000000000000000000000000000000000000000000000000000005000000000000000700000000000000d32688d3c1f3dfcc8b0b36eac7c89d49af331800bd56248044166fa6699442c10000000000000000000000000000000000000000000000000000000000000000718c2f1a0efbd513e016fafd6cf62a624442f2d83708d4b33ab5a8d8c1cd4dd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b1fcb9087762c10418e2a0e9e0791f9fdfe1e123b00416a477cf0875f98e44070000000000000000000000000000000000000000000000000000000000000000", + "custom_data": "504f5748534d3a352e343a3a7367788d5dbf3ca886a9d849228e154693cdbab15d109f6327a71b5ef5860a9b828bef0c4d091913d39750dc8975adbdd261bd10c1c2e110faa47cfbe30e740895552bbdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea4209b00000000000000000000000000000000", + "signature": "3046022100a4ec02ec2714b7c5c23cf6ff85ea45a4cff357199ed093212488ec4efead26d602210094d383e55f079ad3a66dcbfc2962b006b8d98c7a872721a4d54644096dc21bd3", + "signed_by": "attestation" + }, + { + "name": "attestation", + "type": "sgx_attestation_key", + "message": "0e0e100fffff0100000000000000000000000000000000000000000000000000000000000000000000000000000000001500000000000000e70000000000000096b347a64e5a045e27369c26e6dcda51fd7c850e9b3a3a79e718f43261dee1e400000000000000000000000000000000000000000000000000000000000000008c4f5775d796503e96137f77c68a829a0056ac8ded70140b081b094490c57bff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001fe721d0322954821589237fd27efb8fef1acb3ecd6b0352c31271550fc70f940000000000000000000000000000000000000000000000000000000000000000", + "key": "04a024cb34c90ea6a8f9f2181c9020cbcc7c073e69981733c8deed6f6c451822aa08376350ff7da01f842bb40c631cbb711f8b6f7a4fae398320a3884774d250ad", + "auth_data": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "signature": "304502201f14d532274c4385fc0019ca2a21e53e17143cb62377ca4fcdd97fa9fef8fb2502210095d4ee272cf3c512e36779de67dc7814982f1160d981d138a32b265e928a0562", + "signed_by": "quoting_enclave" + }, + { + "name": "quoting_enclave", + "type": "x509_pem", + "message": "MIIE8zCCBJigAwIBAgIUfr2dlwN42DBUA9CXIkBlGP2vV3AwCgYIKoZIzj0EAwIw\ncDEiMCAGA1UEAwwZSW50ZWwgU0dYIFBDSyBQbGF0Zm9ybSBDQTEaMBgGA1UECgwR\nSW50ZWwgQ29ycG9yYXRpb24xFDASBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQI\nDAJDQTELMAkGA1UEBhMCVVMwHhcNMjQwMzIzMDQ0NjIxWhcNMzEwMzIzMDQ0NjIx\nWjBwMSIwIAYDVQQDDBlJbnRlbCBTR1ggUENLIENlcnRpZmljYXRlMRowGAYDVQQK\nDBFJbnRlbCBDb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNV\nBAgMAkNBMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKl7\nRDNlsZKkEtAcW7SfCX1JegbvGq4O0rRUt0z/G6fZJsNlpmRwTB4DYkrgkm1t+9Rp\nLwxFX9/kghxiDQm0jqmjggMOMIIDCjAfBgNVHSMEGDAWgBSVb13NvRvh6UBJydT0\nM84BVwveVDBrBgNVHR8EZDBiMGCgXqBchlpodHRwczovL2FwaS50cnVzdGVkc2Vy\ndmljZXMuaW50ZWwuY29tL3NneC9jZXJ0aWZpY2F0aW9uL3Y0L3Bja2NybD9jYT1w\nbGF0Zm9ybSZlbmNvZGluZz1kZXIwHQYDVR0OBBYEFALKV5DF16KnEbSW5QM9ecDq\nBZaHMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMIICOwYJKoZIhvhNAQ0B\nBIICLDCCAigwHgYKKoZIhvhNAQ0BAQQQttJXuiQVwqM4s74g+HxfKTCCAWUGCiqG\nSIb4TQENAQIwggFVMBAGCyqGSIb4TQENAQIBAgEOMBAGCyqGSIb4TQENAQICAgEO\nMBAGCyqGSIb4TQENAQIDAgEDMBAGCyqGSIb4TQENAQIEAgEDMBEGCyqGSIb4TQEN\nAQIFAgIA/zARBgsqhkiG+E0BDQECBgICAP8wEAYLKoZIhvhNAQ0BAgcCAQEwEAYL\nKoZIhvhNAQ0BAggCAQAwEAYLKoZIhvhNAQ0BAgkCAQAwEAYLKoZIhvhNAQ0BAgoC\nAQAwEAYLKoZIhvhNAQ0BAgsCAQAwEAYLKoZIhvhNAQ0BAgwCAQAwEAYLKoZIhvhN\nAQ0BAg0CAQAwEAYLKoZIhvhNAQ0BAg4CAQAwEAYLKoZIhvhNAQ0BAg8CAQAwEAYL\nKoZIhvhNAQ0BAhACAQAwEAYLKoZIhvhNAQ0BAhECAQ0wHwYLKoZIhvhNAQ0BAhIE\nEA4OAwP//wEAAAAAAAAAAAAwEAYKKoZIhvhNAQ0BAwQCAAAwFAYKKoZIhvhNAQ0B\nBAQGAGBqAAAAMA8GCiqGSIb4TQENAQUKAQEwHgYKKoZIhvhNAQ0BBgQQDVe/DXUV\nE4gemtgO5uBpvDBEBgoqhkiG+E0BDQEHMDYwEAYLKoZIhvhNAQ0BBwEBAf8wEAYL\nKoZIhvhNAQ0BBwIBAQAwEAYLKoZIhvhNAQ0BBwMBAQAwCgYIKoZIzj0EAwIDSQAw\nRgIhAJFgf78HggTBtvQPXZJx/3Fm71vCOmt82pce91M2ZAI0AiEAiZMPBbZZmvR2\nv+1mrs76JeglDQ+pK/SLN94l4+jM5DA=", + "signed_by": "platform_ca" + }, + { + "name": "platform_ca", + "type": "x509_pem", + "message": "MIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC\nMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD\nb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw\nCQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg\nBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs\nIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex\nCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO\n2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJl\neTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS\nBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy\ndmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d\nzb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1Ue\nnA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+", + "signed_by": "sgx_root" + } + ] +} +``` + +Following is an explanation of the different components in the example: + +- The `version` field just indicates the version of the format (`2`), which determines the semantics of the rest of the file. +- The `targets` field is a string array indicating which elements are to be validated. In this case, only the `quote` element is to be validated. +- The `elements` is an array containing each of the elements of the certificate. The role of each of the elements' mandatory fields is explained below: + - The `name` field is a unique identifier for the element throughout the file. It allows for referencing from the `targets` and `elements.signed_by` fields. As opposed to what happens in version 1, arbitrary names are allowed here. + - The `type` field indicates the type of element being described, and dictates which other attributes should also be present. Currently, there are three element types allowed: `sgx_quote`, `sgx_attestation_key` and `x509_pem`. + - The `signed_by` contains either the name of another element within the file (e.g., `platform_ca` for the `quoting_enclave` element), or the value `sgx_root`. It is used to find the certifier for the element at hand. In the case of referencing an element, that element's public key is used for validation of the current element. In the case of `sgx_root`, the root of trust certificate (normally Intel) is to be used for validation. This certificate can be fed manually through e.g. tooling. + - The `message` contains: + - For the `sgx_quote` type, the hex-encoded message signed in that element, that corresponds to a `sgx_quote_t` struct, without the `signature_len` component (see [the source](https://github.com/openenclave/openenclave/blob/master/include/openenclave/bits/sgx/sgxtypes.h) for details). + - For the `sgx_attestation_key` type, the hex-encoded message signed in that element, that corresponds to a `sgx_report_body_t` struct (see [the source](https://github.com/openenclave/openenclave/blob/master/include/openenclave/bits/sgx/sgxtypes.h) for details). + - For the `x509_pem` type, the base64-encoded x509 certificate (same as what a `.pem` file would contain, without the begin and end markers). + - For the `sgx_quote` and `sgx_attestation_key` types, the `signature` contains the hex-encoded DER signature for that element's `message`. + - Additionally, for the `sgx_quote` type, the `custom_data` field contains the hex-encoded custom message given by the powHSM enclave. Its hash is contained within the `message` field. + - Additionally, for the `sgx_attestation_key` type: + - The `key` field contains the hex-encoded uncompressed NIST P-256 attestation public key that is used to validate other elements. + - The `auth_data` field contains hex-encoded additional data that, SHA-256 hashed alongside the aforementioned public key, is contained within the signed `message`. + +The validation process _for each of the targets_ is fairly straightforward, and very similar in spirit to that of the version one certificate shown before. It can also be done manually with the aid of basic ECDSA, hashing and x509 tools: walk the element chain "upwards" until the element signed by `sgx_root` is found. Then, if the element is of `x509_type`, validate it using an x509 parser and validator, retrieving the root of trust accordingly. Otherwise, validate that element's `signature` and `message` against the public key of the `signed_by` element. Additionally, validate that all remaining attributes are contained within the `message` according to its type. Repeat the process walking the chain "downwards" until the target is reached. Fail if at any point an element is deemed invalid. Otherwise the target is valid. Normally the target should be of `sgx_quote` type, and the custom message signed by the powHSM enclave is exactly the `custom_data` field. The individual fields contained within can now be extracted, analysed and validated accordingly. + ## Tooling -For completion's sake, a validation tool is provided within the administration toolset. So, for example, if the file depicted above was at `/a/path/to/the/attestation.json`, the JSON-formatted public keys generated at onboarding time were at `/a/path/to/the/public-keys.json` and we knew the issuer public key was `0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f818057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609` (the actual ledger issuer public key, found in [ledger's endorsement setup tooling](https://github.com/LedgerHQ/blue-loader-python/blob/0.1.31/ledgerblue/endorsementSetup.py#L138)), we could issue: +For completion's sake, validation tools are provided within the administration toolset. So, for example, if the JSON attestation file was at `/a/path/to/the/attestation.json`, the JSON-formatted public keys generated at onboarding time were at `/a/path/to/the/public-keys.json`, then we could issue the following commands depending on the platform. + +### Ledger + +Assuming we knew the issuer public key was `0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f818057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609` (the actual ledger issuer public key, found in [ledger's endorsement setup tooling](https://github.com/LedgerHQ/blue-loader-python/blob/0.1.31/ledgerblue/endorsementSetup.py#L138)), we could issue: ```bash middleware/term> python adm_ledger.py verify_attestation -t /a/path/to/the/attestation.json -r 0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f818057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609 -b /a/path/to/the/public-keys.json @@ -191,8 +289,48 @@ Platform: led UD value: 13c3581aa97c8169d3994e9369c11ebd63bcf123d0671634f21b568983d32916 Best block: bdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea4209b Last transaction signed: 659a04529d6811dd -Timestamp: 0000000000000000 +Timestamp: 0 --------------------------------------------------------------------------------------- ``` -and verify that the reported UI and Signer application hashes match the expected value. Additionally, the user should check that each additional reported value corresponds with an expected or reasonable value (e.g., verify that the UD value corresponds to an RSK block header hash that was mined on or after the time of setup/update; or that in the case of an update, the public keys correspond to those of the powPeg member and have not been altered from the values obtained at setup). \ No newline at end of file +and verify that the reported UI and Signer application hashes match the expected value. The user should also check that each additional reported value corresponds with an expected or reasonable value (e.g., verify that the UD value corresponds to an RSK block header hash that was mined on or after the time of setup/update; or that in the case of an update, the public keys correspond to those of the PowPeg member and have not been altered from the values obtained at setup). + +### SGX + +Assuming we knew Intel SGX Root CA certificate could be downloaded from `https://certificates.trustedservices.intel.com/Intel_SGX_Provisioning_Certification_RootCA.pem` (the actual certificate, that can be found [in Intel's API documentation](https://api.portal.trustedservices.intel.com/content/documentation.html#pcs)), we could issue: + +```bash +middleware/term> python adm_sgx.py verify_attestation -t /a/path/to/the/attestation.json -r https://certificates.trustedservices.intel.com/Intel_SGX_Provisioning_Certification_RootCA.pem -b /a/path/to/the/public-keys.json +``` + +to then obtain the following sample output: + +``` +################################ +### -> Verify powHSM attestation +################################ +Attempting to gather root authority from https://certificates.trustedservices.intel.com/Intel_SGX_Provisioning_Certification_RootCA.pem... +Attempting to validate self-signed root authority... +Using https://certificates.trustedservices.intel.com/Intel_SGX_Provisioning_Certification_RootCA.pem as root authority +-------------------------------------------------------------------------------------------- +powHSM verified with public keys: +m/44'/0'/0'/0/0: 03d2c1ab7245b1676e7aa66ef7588c3925ff972cce19756e6c030ad8ad22634fa4 +m/44'/1'/0'/0/0: 03c9b0dac136c1651e75456f768c6ed3a424500af139905710882f7821c5810ffe +m/44'/1'/1'/0/0: 03b70f79eb845c76bb3c51e0b6c6b58a67ec84bb1fb48871127960f0cfe41dc359 +m/44'/1'/2'/0/0: 031df2601f232cbf1fd8bb5e3dd1fe0bc5c4952b41716546f7c48823dffaa055dc +m/44'/137'/0'/0/0: 0238ad6df3f4023502860c46fab39a64e4ff76225782321eb19be87008606175c4 +m/44'/137'/1'/0/0: 03d4b5cef399724fa0bb27f3e46d83b4f7c3ce69abfebd6afa25f8aa3078a3ac72 +Hash: 0c4d091913d39750dc8975adbdd261bd10c1c2e110faa47cfbe30e740895552b + +Installed powHSM MRENCLAVE: d32688d3c1f3dfcc8b0b36eac7c89d49af331800bd56248044166fa6699442c1 +Installed powHSM MRSIGNER: 718c2f1a0efbd513e016fafd6cf62a624442f2d83708d4b33ab5a8d8c1cd4dd0 +Installed powHSM version: 5.4 +Platform: sgx +UD value: 13c3581aa97c8169d3994e9369c11ebd63bcf123d0671634f21b568983d32916 +Best block: bdcb3c17c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea4209b +Last transaction signed: 659a04529d6811dd +Timestamp: 0 +-------------------------------------------------------------------------------------------- +``` + +and verify that the reported MRENCLAVE and MRSIGNER application hashes match the expected values (for completion, this can be obtained from the Rootstocklabs publicly available enclave binary for the corresponding version, and then its digest verified against a local build). The user should also check that each additional reported value corresponds with an expected or reasonable value (e.g., verify that the UD value corresponds to an RSK block header hash that was mined on or after the time of setup and that the public keys correspond to those that will be used to define the PowPeg member and have not been altered). \ No newline at end of file diff --git a/firmware/build/README.md b/firmware/build/README.md index 267f8ab5..4db0db1c 100644 --- a/firmware/build/README.md +++ b/firmware/build/README.md @@ -72,6 +72,10 @@ For example, to build host and enclave with checkpoint `0x00f06dcff26ec8b4d373fb Once the build is complete, the binaries will be placed under `/firmware/src/sgx/bin` with the names `hsmsgx` for the host and `hsmsgx_enclave.signed` for the signed enclave. +### Reproducible builds + +It is *very important* to mention that both the host and enclave builds are bitwise reproducible. That is, two independent builds of the same code will yield the exact same binary files (and thus, the same `sha256sum`s and `oesign` digests). As a consequence, two independent builds of the same enclave sources signed with the same private key and enclave configuration will also yield two enclave binaries with identical `MRENCLAVE` values. This is paramount for the [attestation process](../../docs/attestation.md). + ### Simulation and debug builds There are also debug and simulation builds available for development and testing purposes. Just replace the use of the `build-sgx` script with either `build-sgx-debug` or `build-sgx-sim` to obtain a debug or simulation version. The debug version has got a slightly different OpenEnclave configuration file and logging settings, and the simulation version can be ran on non-SGX environments (this latter version extremely useful for local development and testing). From 2652b3dee9c97d27b3a2461c09f91c8d3e3a9f92 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Wed, 15 Jan 2025 06:39:31 +1300 Subject: [PATCH 20/21] SGX heartbeat documentation (#259) - Added note to heartbeat documentation stating that it is currently unsupported for SGX - Added corresponding unsupported notes in protocol operations within the protocol documentation --- docs/heartbeat.md | 2 +- docs/protocol.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/heartbeat.md b/docs/heartbeat.md index 58ec43ba..bb41ea6a 100644 --- a/docs/heartbeat.md +++ b/docs/heartbeat.md @@ -2,7 +2,7 @@ ## Foreword -Currently, just like what happens in the case of [attestation](./attestation.md), heartbeat is a feature supported only in the Ledger version of powHSM. A heartbeat implementation for the SGX version of powHSM is currently under development. Therefore, all the information contained herein must be interpreted as applying exclusively to the Ledger version of powHSM. +Currently, heartbeat is a feature supported only in the Ledger version of powHSM. A heartbeat implementation for the SGX version of powHSM is currently under development. Therefore, all the information contained herein must be interpreted as applying exclusively to the Ledger version of powHSM. ## Abstract diff --git a/docs/protocol.md b/docs/protocol.md index 9a7bf889..f3544967 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -356,6 +356,8 @@ This operation can return `0` and generic errors. See the error codes section fo **Error codes:** This operation can return `0`, `-301` and generic errors. See the error codes section for details. +**Important:** +Currently, this operation is unsupported for the SGX version of powHSM, returning error `-905` upon an otherwise correct invocation. ### UI heartbeat @@ -390,6 +392,8 @@ This operation can return `0`, `-301` and generic errors. See the error codes se **Error codes:** This operation can return `0`, `-301` and generic errors. See the error codes section for details. +**Important:** +Currently, this operation is unsupported for the SGX version of powHSM, returning error `-905` upon an otherwise correct invocation. ### Error and success codes From e713a729e53e8a23723300a51dfa5f93d84a66ac Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Wed, 22 Jan 2025 09:39:55 +1300 Subject: [PATCH 21/21] Merge latest sgx changes (#271) * Fixes coverage report for feature/sgx branch (#224) - Triggers the coverage workflow for pushes to master and feature/sgx branches - Adds optional exec argument unit tests scripts - Some additional fixes to unit tests * Install SGX powHSM as a systemd service (#226) * Version 5.3.2 ALPHA release (#227) - Bumped version to 5.3.2 - Updated version references in firmware, middleware and unit tests - Updated CHANGELOG * Fix middleware docker image build (#252) * Fixed C linting to include sgx code (#261) - Including sgx code in lint-c/format-c scripts - Fixed reported sgx linting errors * Sets uninitialized socket file descriptors to -1 (#248) Sets the value of uninitialized file descriptors on io.c to -1 instead of 0. * Removes .gitignore from distribution builds (#264) * Fixes signature for finalise function (#249) Fixes the signature of finalise function so that it conforms with the expected signal handler function signature * Fixes formatting error identified by C linter (#266) * Sanitizes key for kvstore (#247) - Sanitizes the key before using it for file operations. - Added unit tests for keyvalue_store module * Added APDU buffer pointer validation to SGX enclave init sequence (#267) - Using oe_is_outside_enclave to validate the APDU buffer in system_init - Added and updated unit tests cases * Moves finalise logic out of signal handler (#268) Signal handler now only sets a flag that is checked in main * Fixed C linting errors --------- Co-authored-by: Italo Sampaio <100376888+italo-sampaio@users.noreply.github.com> --- CHANGELOG.md | 10 + build-dist-ledger | 1 + build-dist-sgx | 1 + dist/sgx/scripts/setup | 6 + firmware/src/ledger/ui/src/defs.h | 2 +- .../src/ledger/ui/test/onboard/test_onboard.c | 4 +- firmware/src/powhsm/src/defs.h | 2 +- firmware/src/sgx/src/trusted/ecall.c | 1 - firmware/src/sgx/src/trusted/sync.c | 3 +- firmware/src/sgx/src/trusted/system.c | 27 +- firmware/src/sgx/src/trusted/system.h | 7 +- .../src/sgx/src/untrusted/enclave_provider.c | 15 +- .../src/sgx/src/untrusted/enclave_provider.h | 8 +- .../src/sgx/src/untrusted/enclave_proxy.c | 19 +- firmware/src/sgx/src/untrusted/io.c | 45 ++- firmware/src/sgx/src/untrusted/io.h | 6 +- .../src/sgx/src/untrusted/keyvalue_store.c | 60 +++- .../src/sgx/src/untrusted/keyvalue_store.h | 19 +- firmware/src/sgx/src/untrusted/log.c | 8 +- firmware/src/sgx/src/untrusted/log.h | 4 +- firmware/src/sgx/src/untrusted/main.c | 26 +- firmware/src/sgx/test/common/common.mk | 7 +- .../src/sgx/test/common/openenclave/enclave.h | 27 ++ firmware/src/sgx/test/keyvalue_store/Makefile | 38 +++ .../test/keyvalue_store/test_keyvalue_store.c | 283 ++++++++++++++++++ firmware/src/sgx/test/run-all.sh | 2 +- firmware/src/sgx/test/system/test_system.c | 36 ++- lint-c | 3 +- middleware/ledger/protocol.py | 4 +- middleware/tests/ledger/test_protocol.py | 2 +- middleware/tests/ledger/test_protocol_v1.py | 2 +- 31 files changed, 570 insertions(+), 108 deletions(-) create mode 100644 firmware/src/sgx/test/common/openenclave/enclave.h create mode 100644 firmware/src/sgx/test/keyvalue_store/Makefile create mode 100644 firmware/src/sgx/test/keyvalue_store/test_keyvalue_store.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e2bdcc..a74724b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [5.3.2 ALPHA] - 04/12/2024 + +### Features/enhancements + +- SGX distribution: installing powHSM as a system service + +### Fixes + +- Fixed code coverage report + ## [5.3.1 ALPHA] - 14/11/2024 ### Fixes diff --git a/build-dist-ledger b/build-dist-ledger index c62d0297..22959902 100755 --- a/build-dist-ledger +++ b/build-dist-ledger @@ -35,6 +35,7 @@ echo -e "\e[32mBuilding into \e[93m$DEST_DIR\e[32m with checkpoint \e[93m$CHECKP echo -e "\e[33mCopying files and creating directories...\e[0m" rm -rf $DEST_DIR cp -Rf $ROOT_DIR/dist/ledger $DEST_DIR +rm $DEST_DIR/.gitignore rm -rf $FIRMWARE_DIR mkdir -p $FIRMWARE_DIR diff --git a/build-dist-sgx b/build-dist-sgx index a450cbe6..406206fd 100755 --- a/build-dist-sgx +++ b/build-dist-sgx @@ -33,6 +33,7 @@ echo -e "\e[32mBuilding into \e[93m$DEST_DIR\e[32m with checkpoint \e[93m$CHECKP echo -e "\e[33mCopying files and creating directories...\e[0m" rm -rf $DEST_DIR cp -Rf $ROOT_DIR/dist/sgx $DEST_DIR +rm $DEST_DIR/.gitignore rm -rf $BIN_DIR mkdir -p $BIN_DIR diff --git a/dist/sgx/scripts/setup b/dist/sgx/scripts/setup index 3e816f0b..8c7d8eb6 100755 --- a/dist/sgx/scripts/setup +++ b/dist/sgx/scripts/setup @@ -42,6 +42,12 @@ SCRIPTS_DIR=$ROOT_DIR/scripts # Directory where the finalized systemd service unit will be saved SERVICE_DIR=$ROOT_DIR/service +# HSM scripts directory +SCRIPTS_DIR=$ROOT_DIR/scripts + +# Directory where the finalized systemd service unit will be saved +SERVICE_DIR=$ROOT_DIR/service + function checkHsmBinaries() { # Check for HSM binary files FILES="$HSMBIN_DIR/hsmsgx $HSMBIN_DIR/hsmsgx_enclave.signed" diff --git a/firmware/src/ledger/ui/src/defs.h b/firmware/src/ledger/ui/src/defs.h index b4577ec9..9f7ab309 100644 --- a/firmware/src/ledger/ui/src/defs.h +++ b/firmware/src/ledger/ui/src/defs.h @@ -31,6 +31,6 @@ // Version and patchlevel #define VERSION_MAJOR 0x05 #define VERSION_MINOR 0x03 -#define VERSION_PATCH 0x01 +#define VERSION_PATCH 0x02 #endif // __DEFS_H diff --git a/firmware/src/ledger/ui/test/onboard/test_onboard.c b/firmware/src/ledger/ui/test/onboard/test_onboard.c index 933e13b2..bcc8e555 100644 --- a/firmware/src/ledger/ui/test/onboard/test_onboard.c +++ b/firmware/src/ledger/ui/test/onboard/test_onboard.c @@ -313,11 +313,11 @@ void test_is_onboarded() { G_device_onboarded = true; assert(5 == is_onboarded()); - ASSERT_APDU("\x80\x01\x05\x03\x01"); + ASSERT_APDU("\x80\x01\x05\x03\x02"); G_device_onboarded = false; assert(5 == is_onboarded()); - ASSERT_APDU("\x80\x00\x05\x03\x01"); + ASSERT_APDU("\x80\x00\x05\x03\x02"); } int main() { diff --git a/firmware/src/powhsm/src/defs.h b/firmware/src/powhsm/src/defs.h index ac150a22..9465f26a 100644 --- a/firmware/src/powhsm/src/defs.h +++ b/firmware/src/powhsm/src/defs.h @@ -30,6 +30,6 @@ // Version and patchlevel #define VERSION_MAJOR 0x05 #define VERSION_MINOR 0x03 -#define VERSION_PATCH 0x01 +#define VERSION_PATCH 0x02 #endif // __DEFS_H diff --git a/firmware/src/sgx/src/trusted/ecall.c b/firmware/src/sgx/src/trusted/ecall.c index 97f66a76..cdc973bf 100644 --- a/firmware/src/sgx/src/trusted/ecall.c +++ b/firmware/src/sgx/src/trusted/ecall.c @@ -50,4 +50,3 @@ unsigned int ecall_system_process_apdu(unsigned int rx) { SYNC_RELEASE_LOCK(); return result; } - diff --git a/firmware/src/sgx/src/trusted/sync.c b/firmware/src/sgx/src/trusted/sync.c index 078c9eee..179dd58b 100644 --- a/firmware/src/sgx/src/trusted/sync.c +++ b/firmware/src/sgx/src/trusted/sync.c @@ -28,7 +28,8 @@ static bool G_locked = false; bool sync_try_aqcuire_lock() { - if (G_locked) return false; + if (G_locked) + return false; G_locked = true; return true; } diff --git a/firmware/src/sgx/src/trusted/system.c b/firmware/src/sgx/src/trusted/system.c index 02545dc6..51395ea5 100644 --- a/firmware/src/sgx/src/trusted/system.c +++ b/firmware/src/sgx/src/trusted/system.c @@ -1,4 +1,5 @@ #include +#include #include "hal/constants.h" #include "hal/communication.h" @@ -90,13 +91,13 @@ static unsigned int do_unlock(unsigned int rx) { SET_APDU_OP(1); return TX_NO_DATA(); } - + if (APDU_DATA_SIZE(rx) == 0) { THROW(ERR_INVALID_DATA_SIZE); } - SET_APDU_OP( - access_unlock((char*)APDU_DATA_PTR, APDU_DATA_SIZE(rx)) ? 1 : 0); + SET_APDU_OP(access_unlock((char*)APDU_DATA_PTR, APDU_DATA_SIZE(rx)) ? 1 + : 0); return TX_NO_DATA(); } @@ -162,16 +163,25 @@ unsigned int system_process_apdu(unsigned int rx) { return hsm_process_apdu(rx); } -bool system_init(unsigned char *msg_buffer, size_t msg_buffer_size) { +bool system_init(unsigned char* msg_buffer, size_t msg_buffer_size) { // Setup the shared APDU buffer if (msg_buffer_size != EXPECTED_APDU_BUFFER_SIZE) { LOG("Expected APDU buffer size to be %u but got %lu\n", - EXPECTED_APDU_BUFFER_SIZE, msg_buffer_size); + EXPECTED_APDU_BUFFER_SIZE, + msg_buffer_size); + return false; + } + + // Validate that the APDU buffer is entirely outside the enclave + // memory space + if (!oe_is_outside_enclave(msg_buffer, msg_buffer_size)) { + LOG("APDU buffer memory area not outside the enclave\n"); return false; } + apdu_buffer = msg_buffer; apdu_buffer_size = msg_buffer_size; - + // Initialize modules LOG("Initializing modules...\n"); if (!sest_init()) { @@ -210,9 +220,8 @@ bool system_init(unsigned char *msg_buffer, size_t msg_buffer_size) { } nvmem_init(); - if (!nvmem_register_block("bcstate", - &N_bc_state_var, - sizeof(N_bc_state_var))) { + if (!nvmem_register_block( + "bcstate", &N_bc_state_var, sizeof(N_bc_state_var))) { LOG("Error registering bcstate block\n"); return false; } diff --git a/firmware/src/sgx/src/trusted/system.h b/firmware/src/sgx/src/trusted/system.h index d07a1fd4..3928d9c5 100644 --- a/firmware/src/sgx/src/trusted/system.h +++ b/firmware/src/sgx/src/trusted/system.h @@ -27,7 +27,7 @@ /** * @brief Initializes the system module - * + * * @param msg_buffer the APDU buffer to use * @param msg_buffer_size the size of the APDU buffer in bytes * @@ -42,12 +42,11 @@ void system_finalise(); /** * @brief Process an APDU message - * + * * @param rx number of received bytes - * + * * @returns number of bytes to transmit */ unsigned int system_process_apdu(unsigned int rx); - #endif // __TRUSTED_SYSTEM_H diff --git a/firmware/src/sgx/src/untrusted/enclave_provider.c b/firmware/src/sgx/src/untrusted/enclave_provider.c index 20ac295c..5866ee94 100644 --- a/firmware/src/sgx/src/untrusted/enclave_provider.c +++ b/firmware/src/sgx/src/untrusted/enclave_provider.c @@ -22,7 +22,6 @@ * IN THE SOFTWARE. */ - #include #include "hsm_u.h" @@ -36,7 +35,8 @@ #define CREATE_ENCLAVE_FLAGS OE_ENCLAVE_FLAG_SIMULATE #endif -// Global pointer to the enclave. This should be the only global pointer to the enclave +// Global pointer to the enclave. This should be the only global pointer to the +// enclave static char* G_enclave_path = NULL; static oe_enclave_t* G_enclave = NULL; @@ -51,13 +51,18 @@ bool epro_init(char* enclave_path) { oe_enclave_t* epro_get_enclave() { if (NULL == G_enclave) { - oe_enclave_t *enclave = NULL; + oe_enclave_t* enclave = NULL; LOG("Creating HSM enclave...\n"); oe_result_t result = oe_create_hsm_enclave(G_enclave_path, OE_ENCLAVE_TYPE_AUTO, - CREATE_ENCLAVE_FLAGS, NULL, 0, &enclave); + CREATE_ENCLAVE_FLAGS, + NULL, + 0, + &enclave); if (OE_OK != result) { - LOG("Failed to create enclave: oe_result=%u (%s)\n", result, oe_result_str(result)); + LOG("Failed to create enclave: oe_result=%u (%s)\n", + result, + oe_result_str(result)); return NULL; } diff --git a/firmware/src/sgx/src/untrusted/enclave_provider.h b/firmware/src/sgx/src/untrusted/enclave_provider.h index 37a31f91..3adf6f17 100644 --- a/firmware/src/sgx/src/untrusted/enclave_provider.h +++ b/firmware/src/sgx/src/untrusted/enclave_provider.h @@ -29,16 +29,16 @@ /** * @brief Initializes the enclave provider with the given enclave binary path - * + * * @returns Whether initialization succeeded */ bool epro_init(char* enclave_path); /** - * @brief Returns a pointer to the HSM enclave. This function should always - * return a valid pointer to the enclave, which can be used to perform + * @brief Returns a pointer to the HSM enclave. This function should always + * return a valid pointer to the enclave, which can be used to perform * ecall operations. - * + * * @returns A valid pointer to the HSM enclave, or NULL if an error occurred */ oe_enclave_t* epro_get_enclave(); diff --git a/firmware/src/sgx/src/untrusted/enclave_proxy.c b/firmware/src/sgx/src/untrusted/enclave_proxy.c index ee901026..8dad423c 100644 --- a/firmware/src/sgx/src/untrusted/enclave_proxy.c +++ b/firmware/src/sgx/src/untrusted/enclave_proxy.c @@ -22,8 +22,8 @@ * ECALLS */ -bool eprx_system_init(unsigned char *msg_buffer, size_t msg_buffer_size) { - oe_enclave_t *enclave = epro_get_enclave(); +bool eprx_system_init(unsigned char* msg_buffer, size_t msg_buffer_size) { + oe_enclave_t* enclave = epro_get_enclave(); if (enclave == NULL) { LOG("Failed to retrieve the enclave. " "Unable to call system_init().\n"); @@ -31,14 +31,14 @@ bool eprx_system_init(unsigned char *msg_buffer, size_t msg_buffer_size) { } bool result; - oe_result_t oe_result = ecall_system_init(enclave, &result, - msg_buffer, msg_buffer_size); + oe_result_t oe_result = + ecall_system_init(enclave, &result, msg_buffer, msg_buffer_size); CHECK_ECALL_RESULT(oe_result, "Failed to call system_init()", false); return result; } void eprx_system_finalise() { - oe_enclave_t *enclave = epro_get_enclave(); + oe_enclave_t* enclave = epro_get_enclave(); if (enclave == NULL) { LOG("Failed to retrieve the enclave. " "Unable to call system_finalise().\n"); @@ -48,12 +48,13 @@ void eprx_system_finalise() { oe_result_t oe_result = ecall_system_finalise(enclave); if (OE_OK != oe_result) { LOG("Failed to call system_finalise(): oe_result=%u (%s)\n", - oe_result, oe_result_str(oe_result)); + oe_result, + oe_result_str(oe_result)); } } unsigned int eprx_system_process_apdu(unsigned int rx) { - oe_enclave_t *enclave = epro_get_enclave(); + oe_enclave_t* enclave = epro_get_enclave(); if (enclave == NULL) { LOG("Failed to retrieve the enclave. " "Unable to call system_process_command().\n"); @@ -63,7 +64,8 @@ unsigned int eprx_system_process_apdu(unsigned int rx) { unsigned int result; oe_result_t oe_result = ecall_system_process_apdu(enclave, &result, rx); - CHECK_ECALL_RESULT(oe_result, "Failed to call ecall_system_process_apdu()", false); + CHECK_ECALL_RESULT( + oe_result, "Failed to call ecall_system_process_apdu()", false); return result; } @@ -100,4 +102,3 @@ bool ocall_kvstore_remove(char* key) { log_clear_prefix(); return retval; } - diff --git a/firmware/src/sgx/src/untrusted/io.c b/firmware/src/sgx/src/untrusted/io.c index b5dd84b4..deb02a78 100644 --- a/firmware/src/sgx/src/untrusted/io.c +++ b/firmware/src/sgx/src/untrusted/io.c @@ -43,6 +43,13 @@ int serverfd; int connfd; struct sockaddr_in servaddr, cliaddr; +static void close_and_reset_fd(int *fd) { + if (fd && (*fd != -1)) { + close(*fd); + *fd = -1; + } +} + static int start_server(int port, const char *host) { int sockfd; struct hostent *hostinfo; @@ -50,14 +57,14 @@ static int start_server(int port, const char *host) { if (hostinfo == NULL) { LOG("Host not found.\n"); - return 0; + return -1; } // socket create and verification sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { LOG("Socket creation failed...\n"); - return 0; + return -1; } explicit_bzero(&servaddr, sizeof(servaddr)); @@ -65,12 +72,12 @@ static int start_server(int port, const char *host) { if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) < 0) { LOG("Socket option setting failed failed\n"); - return 0; + return -1; } if (setsockopt(sockfd, SOL_TCP, TCP_NODELAY, &(int){1}, sizeof(int)) < 0) { LOG("Socket option setting failed failed\n"); - return 0; + return -1; } // Set address and port @@ -81,13 +88,13 @@ static int start_server(int port, const char *host) { // Binding newly created socket to given IP and verification if ((bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) != 0) { LOG("Socket bind failed...\n"); - return 0; + return -1; } // Now server is ready to listen and verification if ((listen(sockfd, 5)) != 0) { LOG("Listen failed...\n"); - return 0; + return -1; } LOG("Server listening...\n"); @@ -107,20 +114,14 @@ static bool accept_connection() { } bool io_init(int port, const char *host) { - connfd = 0; + connfd = -1; serverfd = start_server(port, host); - return serverfd; + return (serverfd != -1); } void io_finalise() { - if (connfd) { - close(connfd); - connfd = 0; - } - if (serverfd) { - close(serverfd); - serverfd = 0; - } + close_and_reset_fd(&connfd); + close_and_reset_fd(&serverfd); } unsigned short io_exchange(unsigned short tx) { @@ -129,7 +130,7 @@ unsigned short io_exchange(unsigned short tx) { int readlen; while (true) { - if (!connfd) { + if (connfd == -1) { if (!accept_connection()) { LOG("Error accepting client connection\n"); return 0; @@ -146,13 +147,13 @@ unsigned short io_exchange(unsigned short tx) { tx_net = htonl(tx_net); if (send(connfd, &tx_net, sizeof(tx_net), MSG_NOSIGNAL) == -1) { LOG("Connection closed by the client\n"); - connfd = 0; + close_and_reset_fd(&connfd); continue; } // Write APDU if (send(connfd, io_apdu_buffer, tx, MSG_NOSIGNAL) == -1) { LOG("Connection closed by the client\n"); - connfd = 0; + close_and_reset_fd(&connfd); continue; } LOG_HEX("I/O =>", io_apdu_buffer, tx); @@ -172,8 +173,7 @@ unsigned short io_exchange(unsigned short tx) { "Disconnected\n", readlen, rx); - close(connfd); - connfd = 0; + close_and_reset_fd(&connfd); continue; } LOG_HEX("I/O <=", io_apdu_buffer, rx); @@ -196,7 +196,6 @@ unsigned short io_exchange(unsigned short tx) { readlen, sizeof(rx_net)); } - close(connfd); - connfd = 0; + close_and_reset_fd(&connfd); } } diff --git a/firmware/src/sgx/src/untrusted/io.h b/firmware/src/sgx/src/untrusted/io.h index 405fe86a..cc2d0394 100644 --- a/firmware/src/sgx/src/untrusted/io.h +++ b/firmware/src/sgx/src/untrusted/io.h @@ -34,12 +34,12 @@ extern unsigned char io_apdu_buffer[APDU_BUFFER_SIZE]; /** - * @brief Initializes the I/O module. Starts a TCP server at the given host and + * @brief Initializes the I/O module. Starts a TCP server at the given host and * port. - * + * * @param port the port on which to listen for connections * @param host the interface to bind to - * + * */ bool io_init(int port, const char *host); diff --git a/firmware/src/sgx/src/untrusted/keyvalue_store.c b/firmware/src/sgx/src/untrusted/keyvalue_store.c index b743a7ec..b68d6ce2 100644 --- a/firmware/src/sgx/src/untrusted/keyvalue_store.c +++ b/firmware/src/sgx/src/untrusted/keyvalue_store.c @@ -23,20 +23,52 @@ */ #include -#include "hsm_u.h" +#include +#include +#include #include "log.h" +#include "keyvalue_store.h" #define KVSTORE_PREFIX "./kvstore-" #define KVSTORE_SUFFIX ".dat" +#define KVSTORE_MAX_KEY_LEN 150 + +// Sanitizes a key by allowing only [a-zA-Z0-9]. If one or more invalid +// characters are found, Replace them with a single hyphen. +static void sanitize_key(char* key, char* sanitized_key) { + if (!key || !sanitized_key) + return; + + size_t key_len = strlen(key); + + // Truncate key if it's too long + if (key_len > KVSTORE_MAX_KEY_LEN) { + key_len = KVSTORE_MAX_KEY_LEN; + } + + bool prev_char_valid = false; + size_t sanitized_key_len = 0; + for (size_t i = 0; i < key_len; i++) { + if (isalnum(key[i])) { + sanitized_key[sanitized_key_len++] = key[i]; + prev_char_valid = true; + } else if (prev_char_valid) { + sanitized_key[sanitized_key_len++] = '-'; + prev_char_valid = false; + } + } + sanitized_key[sanitized_key_len] = '\0'; +} static char* filename_for(char* key) { - size_t filename_size = strlen(KVSTORE_PREFIX) + - strlen(KVSTORE_SUFFIX) + - strlen(key); - char* filename = malloc(filename_size+1); + char sanitized_key[KVSTORE_MAX_KEY_LEN + 1]; + sanitize_key(key, sanitized_key); + size_t filename_size = + strlen(KVSTORE_PREFIX) + strlen(KVSTORE_SUFFIX) + strlen(sanitized_key); + char* filename = malloc(filename_size + 1); strcpy(filename, ""); strcat(filename, KVSTORE_PREFIX); - strcat(filename, key); + strcat(filename, sanitized_key); strcat(filename, KVSTORE_SUFFIX); return filename; } @@ -45,7 +77,8 @@ static FILE* open_file_for(char* key, char* mode, size_t* file_size) { char* filename = filename_for(key); struct stat fst; stat(filename, &fst); - if (file_size) *file_size = fst.st_size; + if (file_size) + *file_size = fst.st_size; FILE* file = fopen(filename, mode); free(filename); return file; @@ -64,10 +97,7 @@ bool kvstore_save(char* key, uint8_t* data, size_t data_size) { return false; } - if (fwrite(data, - sizeof(data[0]), - data_size, - file) != data_size) { + if (fwrite(data, sizeof(data[0]), data_size, file) != data_size) { LOG("Error writing secret payload for key <%s>\n", key); fclose(file); return false; @@ -109,10 +139,7 @@ size_t kvstore_get(char* key, uint8_t* data_buf, size_t buffer_size) { return 0; } - if (fread(data_buf, - sizeof(data_buf[0]), - file_size, - file) != file_size) { + if (fread(data_buf, sizeof(data_buf[0]), file_size, file) != file_size) { LOG("Could not read payload for key <%s>\n", key); fclose(file); return 0; @@ -125,7 +152,8 @@ size_t kvstore_get(char* key, uint8_t* data_buf, size_t buffer_size) { bool kvstore_remove(char* key) { char* filename = filename_for(key); int result = remove(filename); - if (result) LOG("Error removing file for key <%s>\n", key); + if (result) + LOG("Error removing file for key <%s>\n", key); free(filename); return !result; } diff --git a/firmware/src/sgx/src/untrusted/keyvalue_store.h b/firmware/src/sgx/src/untrusted/keyvalue_store.h index 359d8010..f54de20b 100644 --- a/firmware/src/sgx/src/untrusted/keyvalue_store.h +++ b/firmware/src/sgx/src/untrusted/keyvalue_store.h @@ -25,42 +25,45 @@ #ifndef __KEYVALUE_STORE_H #define __KEYVALUE_STORE_H +#include +#include + /** * @brief Tell whether a given key currently exists - * + * * @param key the key to check for - * + * * @returns whether the key exists */ bool kvstore_exists(char* key); /** * @brief Save the given data to the given key - * + * * @param key the key to save the data to * @param data the buffer containing the data to write * @param data_size the data size in bytes - * + * * @returns whether saving succeeded */ bool kvstore_save(char* key, uint8_t* data, size_t data_size); /** * @brief Read the given key into the given buffer - * + * * @param key the key to read from * @param data_buf the buffer to read the data to * @param buffer_size the buffer size in bytes - * + * * @returns the number of bytes read, or ZERO upon error */ size_t kvstore_get(char* key, uint8_t* data_buf, size_t buffer_size); /** * @brief Remove any data associated with the given key - * + * * @param key the key to remove - * + * * @returns whether key removal was successful */ bool kvstore_remove(char* key); diff --git a/firmware/src/sgx/src/untrusted/log.c b/firmware/src/sgx/src/untrusted/log.c index f8aa9e47..0d4ea99c 100644 --- a/firmware/src/sgx/src/untrusted/log.c +++ b/firmware/src/sgx/src/untrusted/log.c @@ -27,7 +27,7 @@ #include "log.h" -static char* log_prefix = (char*)NULL; +static char *log_prefix = (char *)NULL; void LOG(const char *format, ...) { va_list args; @@ -57,10 +57,10 @@ void LOG_HEX(const char *prefix, const void *buffer, const size_t size) { printf("\n"); } -void log_set_prefix(const char* prefix) { - log_prefix = (char*)prefix; +void log_set_prefix(const char *prefix) { + log_prefix = (char *)prefix; } void log_clear_prefix() { - log_prefix = (char*)NULL; + log_prefix = (char *)NULL; } \ No newline at end of file diff --git a/firmware/src/sgx/src/untrusted/log.h b/firmware/src/sgx/src/untrusted/log.h index 99f16a1d..14850e33 100644 --- a/firmware/src/sgx/src/untrusted/log.h +++ b/firmware/src/sgx/src/untrusted/log.h @@ -43,10 +43,10 @@ void LOG_HEX(const char *prefix, const void *buffer, const size_t size); /** * @brief Set a prefix for all logs - * + * * @param prefix the prefix to use for logs */ -void log_set_prefix(const char* prefix); +void log_set_prefix(const char *prefix); /** * @brief Clear any prefix set for logs diff --git a/firmware/src/sgx/src/untrusted/main.c b/firmware/src/sgx/src/untrusted/main.c index 9251ef1a..fc07d17e 100644 --- a/firmware/src/sgx/src/untrusted/main.c +++ b/firmware/src/sgx/src/untrusted/main.c @@ -45,8 +45,7 @@ static struct argp_option options[] = { {"bind", 'b', "ADDRESS", 0, "Address to bind to", 0}, {"port", 'p', "PORT", 0, "Port to listen on", 0}, - {0} -}; + {0}}; // Argument definitions for argp struct arguments { @@ -55,6 +54,9 @@ struct arguments { char *enclave_path; }; +// Global flag to indicate that the application should stop +static sig_atomic_t G_stop_requested = 0; + // Argp individual option parsing function static error_t parse_opt(int key, char *arg, struct argp_state *state) { struct arguments *arguments = state->input; @@ -91,7 +93,9 @@ static struct argp argp = { parse_opt, "ENCLAVE_PATH", "SGX powHSM", - NULL, NULL, NULL, + NULL, + NULL, + NULL, }; static void finalise_with(int exit_code) { @@ -102,8 +106,12 @@ static void finalise_with(int exit_code) { exit(exit_code); } -static void finalise() { - finalise_with(0); +static void finalise(int signum) { + (void)signum; // Suppress unused parameter warning + + // Note: Do not add any finalise logic directly here, just set the flag + // and let the main loop handle it + G_stop_requested = 1; } static void set_signal_handlers() { @@ -154,6 +162,10 @@ int main(int argc, char **argv) { unsigned int tx = 0; while (true) { + if (G_stop_requested) { + break; + } + rx = io_exchange(tx); if (rx) { @@ -161,9 +173,11 @@ int main(int argc, char **argv) { } } - LOG("Exited main loop unexpectedly\n"); + finalise_with(0); + return 0; main_error: + LOG("Exited main loop unexpectedly\n"); finalise_with(1); return 1; } diff --git a/firmware/src/sgx/test/common/common.mk b/firmware/src/sgx/test/common/common.mk index fbf7a6b1..3b10a292 100644 --- a/firmware/src/sgx/test/common/common.mk +++ b/firmware/src/sgx/test/common/common.mk @@ -1,19 +1,22 @@ TESTCOMMONDIR = ../common SGXTRUSTEDDIR = ../../src/trusted +SGXUNTRUSTEDDIR = ../../src/untrusted HALINCDIR = ../../../hal/include HALSGXSRCDIR = ../../../hal/sgx/src/trusted POWHSMSRCDIR = ../../../powhsm/src COMMONDIR = ../../../common/src -CFLAGS = -iquote $(TESTCOMMONDIR) +CFLAGS = -Wall -Wextra -Werror -Wno-unused-parameter -Wno-unused-function +CFLAGS += -I $(TESTCOMMONDIR) CFLAGS += -iquote $(SGXTRUSTEDDIR) +CFLAGS += -iquote $(SGXUNTRUSTEDDIR) CFLAGS += -iquote $(HALINCDIR) CFLAGS += -iquote $(HALSGXSRCDIR) CFLAGS += -iquote $(POWHSMSRCDIR) CFLAGS += -iquote $(COMMONDIR) CFLAGS += -DHSM_PLATFORM_SGX -VPATH += $(SGXTRUSTEDDIR):$(COMMONDIR) +VPATH += $(SGXTRUSTEDDIR):$(SGXUNTRUSTEDDIR):$(COMMONDIR) include ../../../../coverage/coverage.mk diff --git a/firmware/src/sgx/test/common/openenclave/enclave.h b/firmware/src/sgx/test/common/openenclave/enclave.h new file mode 100644 index 00000000..87b7f344 --- /dev/null +++ b/firmware/src/sgx/test/common/openenclave/enclave.h @@ -0,0 +1,27 @@ +/** + * 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. + */ + +#include + +bool oe_is_outside_enclave(const void *ptr, size_t size); diff --git a/firmware/src/sgx/test/keyvalue_store/Makefile b/firmware/src/sgx/test/keyvalue_store/Makefile new file mode 100644 index 00000000..705a9078 --- /dev/null +++ b/firmware/src/sgx/test/keyvalue_store/Makefile @@ -0,0 +1,38 @@ +# 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. + +include ../common/common.mk + +PROG = test.out +OBJS = keyvalue_store.o test_keyvalue_store.o log.o + +all: $(PROG) + +$(PROG): $(OBJS) + $(CC) $(COVFLAGS) -o $@ $^ + +.PHONY: clean test +clean: + rm -f $(PROG) *.o *.dat $(COVFILES) + +test: all + ./$(PROG) diff --git a/firmware/src/sgx/test/keyvalue_store/test_keyvalue_store.c b/firmware/src/sgx/test/keyvalue_store/test_keyvalue_store.c new file mode 100644 index 00000000..87ff9ae6 --- /dev/null +++ b/firmware/src/sgx/test/keyvalue_store/test_keyvalue_store.c @@ -0,0 +1,283 @@ +/** + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include "keyvalue_store.h" + +// Test helpers +void setup() { + system("rm -f ./kvstore-*.dat"); +} + +void assert_key_exists(char* key, bool exists) { + assert(kvstore_exists(key) == exists); +} + +void assert_key_value(char* key, uint8_t* data, size_t data_size) { + uint8_t retrieved_data[BUFSIZ]; + size_t retrieved_size = + kvstore_get(key, retrieved_data, sizeof(retrieved_data)); + assert(retrieved_size == data_size); + assert(memcmp(retrieved_data, data, retrieved_size) == 0); +} + +void save_and_assert_success(char* key, uint8_t* data, size_t data_size) { + assert(kvstore_save(key, data, data_size)); + assert_key_exists(key, true); +} + +void remove_and_assert_success(char* key) { + assert(kvstore_remove(key)); + assert_key_exists(key, false); +} + +void assert_file_exists(char* filename, bool exists) { + FILE* file = fopen(filename, "rb"); + if (exists) { + assert(file != NULL); + } else { + assert(file == NULL); + } + if (file) { + fclose(file); + } +} + +void assert_file_contents(char* filename, uint8_t* data, size_t data_size) { + FILE* file = fopen(filename, "rb"); + assert(file != NULL); + + uint8_t file_data[BUFSIZ]; + size_t file_size = + fread(file_data, sizeof(file_data[0]), sizeof(file_data), file); + assert(file_size == data_size); + assert(memcmp(file_data, data, data_size) == 0); + + fclose(file); +} + +// Test cases +void test_save_retrieve() { + printf("Test save and retrieve...\n"); + setup(); + + struct { + char* key; + char* data; + } input_data[] = {{"a-key", "some piece of data"}, + {"another-key", "another piece of data"}, + {"yet-another-key", "yet another piece of data"}, + {"the-last-key", "the last piece of data"}}; + size_t num_inputs = sizeof(input_data) / sizeof(input_data[0]); + + for (size_t i = 0; i < num_inputs; i++) { + save_and_assert_success(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + } + + for (size_t i = 0; i < num_inputs; i++) { + assert_key_value(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + } +} + +void test_kvstore_exists() { + printf("Test kvstore_exists...\n"); + setup(); + + struct { + char* key; + char* data; + } existing_keys[] = { + {"first-key", "some piece of data"}, + {"second-key", "another piece of data"}, + {"third-key", "yet another piece of data"}, + }; + size_t num_existing_keys = sizeof(existing_keys) / sizeof(existing_keys[0]); + + char* non_existing_keys[] = { + "non-existing-key-1", + "non-existing-key-2", + "non-existing-key-3", + }; + size_t num_non_existing_keys = + sizeof(non_existing_keys) / sizeof(non_existing_keys[0]); + + for (size_t i = 0; i < num_existing_keys; i++) { + save_and_assert_success(existing_keys[i].key, + (uint8_t*)existing_keys[i].data, + strlen(existing_keys[i].data)); + } + + for (size_t i = 0; i < num_existing_keys; i++) { + assert_key_exists(existing_keys[i].key, true); + } + + for (size_t i = 0; i < num_non_existing_keys; i++) { + assert_key_exists(non_existing_keys[i], false); + } +} + +void test_save_remove() { + printf("Test save and remove...\n"); + setup(); + + struct { + char* key; + char* data; + bool remove; + } input_data[] = { + {"first-key", "some piece of data", false}, + {"second-key", "another piece of data", true}, + {"third-key", "yet another piece of data", true}, + {"fourth-key", "the last piece of data", false}, + }; + size_t num_inputs = sizeof(input_data) / sizeof(input_data[0]); + + for (size_t i = 0; i < num_inputs; i++) { + save_and_assert_success(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + assert_key_value(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + } + + // Remove selected keys + for (size_t i = 0; i < num_inputs; i++) { + if (input_data[i].remove) { + remove_and_assert_success(input_data[i].key); + } + } + + // Assert that the selected keys were removed and the others still exist + for (size_t i = 0; i < num_inputs; i++) { + if (input_data[i].remove) { + assert_key_exists(input_data[i].key, false); + } else { + assert_key_value(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + } + } +} + +void test_filename() { + printf("Test filename for key...\n"); + setup(); + + struct { + char* key; + char* data; + char* filename; + } input_data[] = { + {"first-key", "data for the first key", "kvstore-first-key.dat"}, + {"second-key", "data for the second key", "kvstore-second-key.dat"}, + {"third-key", "data for the third key", "kvstore-third-key.dat"}, + {"fourth-key", "data for the fourth key", "kvstore-fourth-key.dat"}, + }; + size_t num_inputs = sizeof(input_data) / sizeof(input_data[0]); + + // Make sure none of the files exist + for (size_t i = 0; i < num_inputs; i++) { + assert_file_exists(input_data[i].filename, false); + } + + // Save data to each key and assert that the file name and contents are + // correct + for (size_t i = 0; i < num_inputs; i++) { + save_and_assert_success(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + assert_file_exists(input_data[i].filename, true); + assert_file_contents(input_data[i].filename, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + } +} + +void test_sanitize_key() { + printf("Test sanitize key...\n"); + setup(); + + struct { + char* key; + char* filename; + char* data; + } input_data[] = { + {"onlyletters", "kvstore-onlyletters.dat", "data1"}, + {"123456", "kvstore-123456.dat", "data2"}, + {"lettersandnumbers123", "kvstore-lettersandnumbers123.dat", "data3"}, + {"letters-and-numbers-with-hyphen-123", + "kvstore-letters-and-numbers-with-hyphen-123.dat", + "data4"}, + {"key containing spaces", "kvstore-key-containing-spaces.dat", "data5"}, + {"key containing special characters!@#$%^&*()", + "kvstore-key-containing-special-characters-.dat", + "data6"}, + {"../../../../../etc/passwd", "kvstore-etc-passwd.dat", "data7"}, + {"some@#£_&-(_./file#£+-:;name", "kvstore-some-file-name.dat", "data8"}, + }; + size_t num_inputs = sizeof(input_data) / sizeof(input_data[0]); + + // Make sure none of the files exist + for (size_t i = 0; i < num_inputs; i++) { + assert_file_exists(input_data[i].filename, false); + } + + // Save data to each key and assert that the file name and contents are + // correct + for (size_t i = 0; i < num_inputs; i++) { + save_and_assert_success(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + assert_file_exists(input_data[i].filename, true); + assert_file_contents(input_data[i].filename, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + } + + // Ensure data can be retrieved with the original key + for (size_t i = 0; i < num_inputs; i++) { + assert_key_value(input_data[i].key, + (uint8_t*)input_data[i].data, + strlen(input_data[i].data)); + } +} + +int main() { + test_save_retrieve(); + test_kvstore_exists(); + test_save_remove(); + test_filename(); + test_sanitize_key(); + return 0; +} \ No newline at end of file diff --git a/firmware/src/sgx/test/run-all.sh b/firmware/src/sgx/test/run-all.sh index e69b07d7..e54a9360 100755 --- a/firmware/src/sgx/test/run-all.sh +++ b/firmware/src/sgx/test/run-all.sh @@ -2,7 +2,7 @@ if [[ $1 == "exec" ]]; then BASEDIR=$(realpath $(dirname $0)) - TESTDIRS="system" + TESTDIRS="system keyvalue_store" for d in $TESTDIRS; do echo "******************************" echo "Testing $d..." diff --git a/firmware/src/sgx/test/system/test_system.c b/firmware/src/sgx/test/system/test_system.c index 8ee70584..29761631 100644 --- a/firmware/src/sgx/test/system/test_system.c +++ b/firmware/src/sgx/test/system/test_system.c @@ -65,6 +65,7 @@ typedef struct mock_calls_counter { int nvmem_init_count; int nvmem_register_block_count; int sest_init_count; + int oe_is_outside_enclave_count; } mock_calls_counter_t; typedef struct nvmem_register_block_args { @@ -107,6 +108,7 @@ typedef struct mock_force_fail { bool endorsement_init; bool nvmem_register_block; bool sest_init; + bool oe_is_outside_enclave; } mock_force_fail_t; typedef struct mock_data { @@ -167,6 +169,11 @@ try_context_t* G_try_last_open_context = &G_try_last_open_context_var; unsigned char G_io_apdu_buffer[IO_APDU_BUFFER_SIZE]; // Mock implementation of dependencies +bool oe_is_outside_enclave(const void* ptr, size_t size) { + MOCK_CALL(oe_is_outside_enclave); + return true; +} + void hsm_init() { NUM_CALLS(hsm_init)++; } @@ -356,6 +363,7 @@ void test_init_success() { printf("Test system_init success...\n"); assert(system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); assert(NUM_CALLS(access_init) == 1); assert(NUM_CALLS(seed_init) == 1); @@ -386,6 +394,23 @@ void test_init_fails_invalid_buf_size() { printf("Test system_init fails with invalid buffer size...\n"); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer) - 1)); + ASSERT_NOT_CALLED(oe_is_outside_enclave); + ASSERT_NOT_CALLED(sest_init); + ASSERT_NOT_CALLED(access_init); + ASSERT_NOT_CALLED(seed_init); + ASSERT_NOT_CALLED(communication_init); + ASSERT_NOT_CALLED(endorsement_init); + ASSERT_NOT_CALLED(nvmem_init); + teardown(); +} + +void test_init_fails_invalid_buf_memarea() { + setup(); + printf("Test system_init fails with invalid buffer memory area...\n"); + + FORCE_FAIL(oe_is_outside_enclave, true); + assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); ASSERT_NOT_CALLED(sest_init); ASSERT_NOT_CALLED(access_init); ASSERT_NOT_CALLED(seed_init); @@ -401,6 +426,7 @@ void test_init_fails_when_sest_init_fails() { FORCE_FAIL(sest_init, true); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); ASSERT_NOT_CALLED(access_init); ASSERT_NOT_CALLED(seed_init); @@ -416,6 +442,7 @@ void test_init_fails_when_access_init_fails() { FORCE_FAIL(access_init, true); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); assert(NUM_CALLS(access_init) == 1); ASSERT_NOT_CALLED(seed_init); @@ -431,6 +458,7 @@ void test_init_fails_when_seed_init_fails() { FORCE_FAIL(seed_init, true); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); assert(NUM_CALLS(access_init) == 1); assert(NUM_CALLS(seed_init) == 1); @@ -446,6 +474,7 @@ void test_init_fails_when_communication_init_fails() { FORCE_FAIL(communication_init, true); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); assert(NUM_CALLS(access_init) == 1); assert(NUM_CALLS(seed_init) == 1); @@ -461,6 +490,7 @@ void test_init_fails_when_endorsement_init_fails() { FORCE_FAIL(endorsement_init, true); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); assert(NUM_CALLS(access_init) == 1); assert(NUM_CALLS(seed_init) == 1); @@ -476,7 +506,7 @@ void test_init_fails_when_nvmem_register_block_fails() { FORCE_NVMEM_FAIL_ON_KEY("bcstate"); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); - assert(NUM_CALLS(sest_init) == 1); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); assert(NUM_CALLS(access_init) == 1); assert(NUM_CALLS(seed_init) == 1); @@ -492,6 +522,8 @@ void test_init_fails_when_nvmem_register_block_fails() { FORCE_NVMEM_FAIL_ON_KEY("bcstate_updating"); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 2); + assert(NUM_CALLS(sest_init) == 2); assert(NUM_CALLS(access_init) == 2); assert(NUM_CALLS(seed_init) == 2); assert(NUM_CALLS(communication_init) == 2); @@ -518,6 +550,7 @@ void test_init_fails_when_nvmem_load_fails() { FORCE_FAIL(nvmem_load, true); assert(!system_init(G_io_apdu_buffer, sizeof(G_io_apdu_buffer))); + assert(NUM_CALLS(oe_is_outside_enclave) == 1); assert(NUM_CALLS(sest_init) == 1); assert(NUM_CALLS(access_init) == 1); assert(NUM_CALLS(seed_init) == 1); @@ -990,6 +1023,7 @@ void test_invalid_cmd_not_handled() { int main() { test_init_success(); test_init_fails_invalid_buf_size(); + test_init_fails_invalid_buf_memarea(); test_init_fails_when_sest_init_fails(); test_init_fails_when_access_init_fails(); test_init_fails_when_seed_init_fails(); diff --git a/lint-c b/lint-c index 65270971..6f35653f 100755 --- a/lint-c +++ b/lint-c @@ -12,11 +12,12 @@ if [[ $1 == "exec" ]]; then fi SRC_DIR="firmware/src" - SEARCH_DIRS="$SRC_DIR/ledger/signer $SRC_DIR/ledger/ui $SRC_DIR/tcpsigner $SRC_DIR/common $SRC_DIR/hal" + SEARCH_DIRS="$SRC_DIR/ledger/signer $SRC_DIR/ledger/ui $SRC_DIR/tcpsigner $SRC_DIR/common $SRC_DIR/hal $SRC_DIR/sgx" find $SEARCH_DIRS -name "*.[ch]" | \ egrep -v "(bigdigits|bigdtypes|keccak256)\.[ch]$" | \ egrep -v "firmware/src/ledger/ui/src/glyphs.[ch]" | \ + egrep -v "firmware/src/sgx/src/(trusted|untrusted)/hsm_([tu]|args).[ch]" | \ xargs clang-format-10 --style=file $CLANG_ARGS else # Script directory diff --git a/middleware/ledger/protocol.py b/middleware/ledger/protocol.py index 3b683345..4141cda5 100644 --- a/middleware/ledger/protocol.py +++ b/middleware/ledger/protocol.py @@ -38,8 +38,8 @@ class HSM2ProtocolLedger(HSM2Protocol): # Current manager supported versions for HSM UI and HSM SIGNER (<=) - UI_VERSION = HSM2FirmwareVersion(5, 3, 1) - APP_VERSION = HSM2FirmwareVersion(5, 3, 1) + UI_VERSION = HSM2FirmwareVersion(5, 3, 2) + APP_VERSION = HSM2FirmwareVersion(5, 3, 2) # Amount of time to wait to make sure the app is opened OPEN_APP_WAIT = 1 # second diff --git a/middleware/tests/ledger/test_protocol.py b/middleware/tests/ledger/test_protocol.py index 25a076e2..08079d58 100644 --- a/middleware/tests/ledger/test_protocol.py +++ b/middleware/tests/ledger/test_protocol.py @@ -49,7 +49,7 @@ def setUp(self): self.dongle.disconnect = Mock() self.dongle.is_onboarded = Mock(return_value=True) self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER) - self.dongle.get_version = Mock(return_value=HSM2FirmwareVersion(5, 3, 1)) + self.dongle.get_version = Mock(return_value=HSM2FirmwareVersion(5, 3, 2)) self.dongle.get_signer_parameters = Mock(return_value=Mock( min_required_difficulty=123)) self.protocol = HSM2ProtocolLedger(self.pin, self.dongle) diff --git a/middleware/tests/ledger/test_protocol_v1.py b/middleware/tests/ledger/test_protocol_v1.py index 91978469..b0a4d0d1 100644 --- a/middleware/tests/ledger/test_protocol_v1.py +++ b/middleware/tests/ledger/test_protocol_v1.py @@ -47,7 +47,7 @@ def setUp(self): self.dongle.disconnect = Mock() self.dongle.is_onboarded = Mock(return_value=True) self.dongle.get_current_mode = Mock(return_value=HSM2Dongle.MODE.SIGNER) - self.dongle.get_version = Mock(return_value=HSM2FirmwareVersion(5, 3, 1)) + self.dongle.get_version = Mock(return_value=HSM2FirmwareVersion(5, 3, 2)) self.dongle.get_signer_parameters = Mock(return_value=Mock( min_required_difficulty=123)) self.protocol = HSM1ProtocolLedger(self.pin, self.dongle)