From d57d434d82e3879030a0cc774d359544ac0b4d63 Mon Sep 17 00:00:00 2001 From: Ariel Mendelzon Date: Fri, 22 Nov 2024 15:25:18 -0300 Subject: [PATCH] Enhancing existing attestation scheme with additional information - 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 --- 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 + 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 ++++++++++++ 21 files changed, 720 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/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"), + ])