diff --git a/Makefile b/Makefile index 675a1d9..df9129c 100644 --- a/Makefile +++ b/Makefile @@ -35,10 +35,13 @@ APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" ifeq ($(TARGET_NAME),TARGET_NANOS) DEFINES += MAX_INPUT_COUNT=15 + DEFINES += MAX_MESSAGE_LEN=120 else ifeq ($(TARGET_NAME),TARGET_STAX) DEFINES += MAX_INPUT_COUNT=128 + DEFINES += MAX_MESSAGE_LEN=200 else DEFINES += MAX_INPUT_COUNT=128 + DEFINES += MAX_MESSAGE_LEN=200 endif # Application source files @@ -76,7 +79,7 @@ VARIANT_PARAM = COIN VARIANT_VALUES = KAS # Enabling DEBUG flag will enable PRINTF and disable optimizations -#DEBUG = 1 +DEBUG = 1 ######################################## # Application custom permissions # diff --git a/doc/COMMANDS.md b/doc/COMMANDS.md index 334ad62..f15e093 100644 --- a/doc/COMMANDS.md +++ b/doc/COMMANDS.md @@ -121,6 +121,36 @@ Transactions signed with ECDSA are currently not supported. \* While `has_more` is non-zero, you can ask for the next signature by sending another APDU back +## SIGN_MESSAGE + +### Command + +| CLA | INS | P1 | P2 | Lc | CData | +| --- | --- | --- | --- | --- | --- | +| 0xE0 | 0x07 | 0x00 | 0x00 | var | `address_type (1)` \|\| `address_index (4)` \|\|
`message_len (1 bytes)` \|\| `message (var bytes)` | + +| CData Part | Description | +| --- | --- | +| `address_type` | Either `00` for Receive Address or `01` for Change Address | +| `address_index` | Any value from `00000000` to `11111111` | +| `message_len` | How long the message is. Must be a value from `1` to `128`, inclusive | +| `message` | The message to sign | + +### Response + +| Length
(bytes) | SW | RData | +| --- | --- | --- | +| var | 0x9000 | See Response Breakdown | + +#### Response Breakdown + +| Data | Description | +| --- | --- | +| `len(sig)` | The length of the signature. Always 64 bytes with Schnorr | +| `sig` | The Schnorr signature | +| `len(message_hash)` | The length of the message hash. Always 32 bytes | +| `message_hash` | The hash that was signed. | + ## Status Words | SW | SW name | Description | @@ -143,4 +173,5 @@ Transactions signed with ECDSA are currently not supported. | 0xB00A | `SW_WRONG_BIP32_COIN_TYPE` | `Coin Type` must be `111111'` | | 0xB00B | `SW_WRONG_BIP32_TYPE` | `Type` passed is not valid. Must be either `0` for `Receive` or `1` for `Change`| | 0xB00C | `SW_WRONG_BIP32_PATH_LEN` | Path length must be `5` | +| 0xB00D | `SW_MESSAGE_TOO_LONG` | Message len greater than max | | 0x9000 | `OK` | Success | diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt index f70f1b9..53891ae 100644 --- a/fuzzing/CMakeLists.txt +++ b/fuzzing/CMakeLists.txt @@ -22,7 +22,7 @@ endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -add_compile_definitions(MAX_INPUT_COUNT=15 USB_SEGMENT_SIZE=64) +add_compile_definitions(MAX_INPUT_COUNT=15 MAX_MESSAGE_LEN=200 USB_SEGMENT_SIZE=64) include(extra/TxParser.cmake) diff --git a/src/apdu/dispatcher.c b/src/apdu/dispatcher.c index 771049f..fc1ceeb 100644 --- a/src/apdu/dispatcher.c +++ b/src/apdu/dispatcher.c @@ -35,6 +35,7 @@ #include "../handler/get_app_name.h" #include "../handler/get_public_key.h" #include "../handler/sign_tx.h" +#include "../handler/sign_msg.h" #ifdef HAVE_DEBUG_APDU #include "../handler/debug.h" @@ -92,6 +93,13 @@ int apdu_dispatcher(const command_t *cmd) { buf.offset = 0; return handler_sign_tx(&buf, cmd->p1, (bool) (cmd->p2 & P2_MORE)); + case SIGN_MESSAGE: + + buf.ptr = cmd->data; + buf.size = cmd->lc; + buf.offset = 0; + + return handler_sign_msg(&buf); #ifdef HAVE_DEBUG_APDU case DEBUG_APDU: return handler_debug(cmd->p1); diff --git a/src/common/bip32.c b/src/common/bip32.c deleted file mode 100644 index 18a4af7..0000000 --- a/src/common/bip32.c +++ /dev/null @@ -1,95 +0,0 @@ -/***************************************************************************** - * MIT License - * - * Copyright (c) 2023 coderofstuff - * - * 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 // snprintf -#include // memset, strlen -#include // size_t -#include // uint*_t -#include // bool - -#include "bip32.h" -#include "read.h" - -bool bip32_path_read(const uint8_t *in, size_t in_len, uint32_t *out, size_t out_len) { - if (out_len == 0 || out_len > MAX_BIP32_PATH) { - return false; - } - - size_t offset = 0; - - for (size_t i = 0; i < out_len; i++) { - if (offset > in_len) { - return false; - } - out[i] = read_u32_be(in, offset); - offset += 4; - } - - return true; -} - -bool bip32_path_format(const uint32_t *bip32_path, - size_t bip32_path_len, - char *out, - size_t out_len) { - if (bip32_path_len == 0 || bip32_path_len > MAX_BIP32_PATH) { - return false; - } - - size_t offset = 0; - - for (uint16_t i = 0; i < bip32_path_len; i++) { - size_t written; - - snprintf(out + offset, out_len - offset, "%d", bip32_path[i] & 0x7FFFFFFFu); - written = strlen(out + offset); - if (written == 0 || written >= out_len - offset) { - memset(out, 0, out_len); - return false; - } - offset += written; - - if ((bip32_path[i] & 0x80000000u) != 0) { - snprintf(out + offset, out_len - offset, "'"); - written = strlen(out + offset); - if (written == 0 || written >= out_len - offset) { - memset(out, 0, out_len); - return false; - } - offset += written; - } - - if (i != bip32_path_len - 1) { - snprintf(out + offset, out_len - offset, "/"); - written = strlen(out + offset); - if (written == 0 || written >= out_len - offset) { - memset(out, 0, out_len); - return false; - } - offset += written; - } - } - - return true; -} diff --git a/src/constants.h b/src/constants.h index 4b6a667..151d691 100644 --- a/src/constants.h +++ b/src/constants.h @@ -43,11 +43,6 @@ */ #define MAX_APPNAME_LEN 64 -/** - * Maximum transaction length (bytes). - */ -#define MAX_TRANSACTION_LEN 128 - /** * Maximum signature length (bytes). * Schnorr signatures only have 64 bytes @@ -70,4 +65,6 @@ */ #define SIGNING_KEY "TransactionSigningHash" +#define MESSAGE_SIGNING_KEY "PersonalMessageSigningHash" + #define MAX_OUTPUT_COUNT 2 diff --git a/src/crypto.c b/src/crypto.c index ab22a19..53a2c72 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -31,6 +31,7 @@ #include "globals.h" #include "sighash.h" +#include "personal_message.h" bool crypto_validate_public_key(const uint32_t *bip32_path, uint8_t bip32_path_len, @@ -54,7 +55,7 @@ bool crypto_validate_public_key(const uint32_t *bip32_path, return memcmp(raw_pubkey + 1, compressed_public_key, 32) == 0; } -int crypto_sign_message(void) { +int crypto_sign_transaction(void) { cx_ecfp_private_key_t private_key = {0}; cx_ecfp_public_key_t public_key = {0}; uint8_t chain_code[32] = {0}; @@ -117,3 +118,37 @@ int crypto_sign_message(void) { return error; } + +int crypto_sign_personal_message(void) { + hash_personal_message(G_context.msg_info.message, + G_context.msg_info.message_len, + G_context.msg_info.message_hash); + + cx_ecfp_private_key_t private_key = {0}; + uint8_t chain_code[32] = {0}; + + int error = bip32_derive_init_privkey_256(CX_CURVE_256K1, + G_context.bip32_path, + G_context.bip32_path_len, + &private_key, + chain_code); + + BEGIN_TRY { + TRY { + size_t sig_len = sizeof(G_context.tx_info.signature); + error = cx_ecschnorr_sign_no_throw(&private_key, + CX_ECSCHNORR_BIP0340 | CX_RND_TRNG, + CX_SHA256, + G_context.msg_info.message_hash, + sizeof(G_context.msg_info.message_hash), + G_context.msg_info.signature, + &sig_len); + } + FINALLY { + explicit_bzero(&private_key, sizeof(private_key)); + } + } + END_TRY; + + return error; +} diff --git a/src/crypto.h b/src/crypto.h index ff05f0a..aa89ed1 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -37,7 +37,7 @@ * @return 0 on success, error number otherwise. * */ -int crypto_sign_message(void); +int crypto_sign_transaction(void); /** * Checks if the compressed public key matches the @@ -56,3 +56,14 @@ int crypto_sign_message(void); bool crypto_validate_public_key(const uint32_t *bip32_path, uint8_t bip32_path_len, uint8_t compressed_public_key[static 32]); + +/** + * Sign personal message hash in global context. + * + * @see G_context.bip32_path, + * G_context.msg_info.signature. + * + * @return 0 on success, error number otherwise. + * + */ +int crypto_sign_personal_message(void); \ No newline at end of file diff --git a/src/handler/sign_msg.c b/src/handler/sign_msg.c new file mode 100644 index 0000000..5700235 --- /dev/null +++ b/src/handler/sign_msg.c @@ -0,0 +1,90 @@ +/***************************************************************************** + * MIT License + * + * Copyright (c) 2023 coderofstuff + * + * 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 // uint*_t +#include // memset, explicit_bzero + +#include "types.h" +#include "buffer.h" +#include "./globals.h" +#include "./sign_msg.h" +#include "../sw.h" +#include "../ui/display.h" +#include "../helper/send_response.h" + +/** + * Handler for SIGN_MESSAGE command. If successfully parse BIP32 path + * and message, sign the message and send APDU response. + * + * @see G_context.bip32_path, G_context.msg_info + * + * @param[in,out] cdata + * Command data with BIP32 path and raw message. + * + * @return zero or positive integer if success, negative integer otherwise. + * + */ +int handler_sign_msg(buffer_t *cdata) { + explicit_bzero(&G_context, sizeof(G_context)); + G_context.req_type = CONFIRM_MESSAGE; + G_context.state = STATE_NONE; + + if (!buffer_read_u8(cdata, &G_context.msg_info.address_type)) { + return io_send_sw(SW_WRONG_DATA_LENGTH); + } + + if (!buffer_read_u32(cdata, &G_context.msg_info.address_index, BE)) { + return io_send_sw(SW_WRONG_DATA_LENGTH); + } + + uint8_t message_len = 0; + if (!buffer_read_u8(cdata, &message_len)) { + return io_send_sw(SW_WRONG_DATA_LENGTH); + } + + if (message_len > MAX_MESSAGE_LEN) { + return io_send_sw(SW_MESSAGE_TOO_LONG); + } + + G_context.msg_info.message_len = (size_t) message_len; + + if (!buffer_can_read(cdata, G_context.msg_info.message_len)) { + return io_send_sw(SW_WRONG_DATA_LENGTH); + } + + memcpy(G_context.msg_info.message, cdata->ptr + cdata->offset, G_context.msg_info.message_len); + + if (!buffer_seek_cur(cdata, G_context.msg_info.message_len)) { + return io_send_sw(SW_WRONG_DATA_LENGTH); + } + + G_context.bip32_path[0] = 0x8000002C; + G_context.bip32_path[1] = 0x8001b207; + G_context.bip32_path[2] = 0x80000000; + G_context.bip32_path[3] = (uint32_t)(G_context.msg_info.address_type); + G_context.bip32_path[4] = G_context.msg_info.address_index; + + G_context.bip32_path_len = 5; + + return ui_display_message(); +} \ No newline at end of file diff --git a/src/common/bip32.h b/src/handler/sign_msg.h similarity index 54% rename from src/common/bip32.h rename to src/handler/sign_msg.h index c306956..f005e10 100644 --- a/src/common/bip32.h +++ b/src/handler/sign_msg.h @@ -23,48 +23,20 @@ *****************************************************************************/ #pragma once -#include // size_t -#include // uint*_t -#include // bool +#include // uint*_t -/** - * Maximum length of BIP32 path allowed. - */ -#define MAX_BIP32_PATH 5 +#include "buffer.h" /** - * Read BIP32 path from byte buffer. - * - * @param[in] in - * Pointer to input byte buffer. - * @param[in] in_len - * Length of input byte buffer. - * @param[out] out - * Pointer to output 32-bit integer buffer. - * @param[in] out_len - * Number of BIP32 paths read in the output buffer. - * - * @return true if success, false otherwise. + * Handler for SIGN_MESSAGE command. If successfully parse BIP32 path + * and message, sign the message and send APDU response. * - */ -bool bip32_path_read(const uint8_t *in, size_t in_len, uint32_t *out, size_t out_len); - -/** - * Format BIP32 path as string. + * @see G_context.bip32_path, G_context.msg_info * - * @param[in] bip32_path - * Pointer to 32-bit integer input buffer. - * @param[in] bip32_path_len - * Maximum number of BIP32 paths in the input buffer. - * @param[out] out string - * Pointer to output string. - * @param[in] out_len - * Length of the output string. + * @param[in,out] cdata + * Command data with BIP32 path and raw message. * - * @return true if success, false otherwise. + * @return zero or positive integer if success, negative integer otherwise. * */ -bool bip32_path_format(const uint32_t *bip32_path, - size_t bip32_path_len, - char *out, - size_t out_len); +int handler_sign_msg(buffer_t *cdata); diff --git a/src/handler/sign_tx.c b/src/handler/sign_tx.c index 7956cbc..36139ac 100644 --- a/src/handler/sign_tx.c +++ b/src/handler/sign_tx.c @@ -43,7 +43,7 @@ #include "../helper/send_response.h" static int sign_input_and_send() { - int error = crypto_sign_message(); + int error = crypto_sign_transaction(); if (error != 0) { G_context.state = STATE_NONE; io_send_sw(error); diff --git a/src/helper/send_reponse.c b/src/helper/send_reponse.c index 2d8ba24..d9e61fd 100644 --- a/src/helper/send_reponse.c +++ b/src/helper/send_reponse.c @@ -68,3 +68,23 @@ int helper_send_response_sig() { return io_send_response_pointer(resp, offset, SW_OK); } + +int helper_send_response_personal_message_sig() { + uint8_t resp[3 + MAX_DER_SIG_LEN + 34] = {0}; + size_t offset = 0; + + // len(sig) -> 1 byte + resp[offset++] = MAX_DER_SIG_LEN; + // sig -> 64 bytes + memmove(resp + offset, G_context.msg_info.signature, MAX_DER_SIG_LEN); + offset += MAX_DER_SIG_LEN; + // len(sighash) -> 1 byte + resp[offset++] = sizeof(G_context.msg_info.message_hash); + // sighash -> 32 bytes + memmove(resp + offset, + G_context.msg_info.message_hash, + sizeof(G_context.msg_info.message_hash)); + offset += sizeof(G_context.msg_info.message_hash); + + return io_send_response_pointer(resp, offset, SW_OK); +} diff --git a/src/helper/send_response.h b/src/helper/send_response.h index 78155dc..49b9695 100644 --- a/src/helper/send_response.h +++ b/src/helper/send_response.h @@ -60,3 +60,13 @@ int helper_send_response_pubkey(void); * */ int helper_send_response_sig(void); + +/** + * Helper to send APDU response with personal message signature + * response = MAX_DER_SIG_LEN (1) || + * G_context.tx_info.signature (MAX_DER_SIG_LEN) + * + * @return zero or positive integer if success, -1 otherwise. + * + */ +int helper_send_response_personal_message_sig(void); diff --git a/src/personal_message.c b/src/personal_message.c new file mode 100644 index 0000000..f9196e1 --- /dev/null +++ b/src/personal_message.c @@ -0,0 +1,72 @@ +/***************************************************************************** + * MIT License + * + * Copyright (c) 2023 coderofstuff + * + * 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 "constants.h" +#include "./import/blake2-impl.h" +#include "./import/blake2b.h" +#include "./personal_message.h" + +static int hash_init(blake2b_state* hash, size_t size, uint8_t* key, size_t key_len) { + if (key == NULL && key_len != 0) { + goto err; + } + + if (size % 8 != 0 || size < 8 || size > 512) { + goto err; + } + memset(hash, 0, sizeof(blake2b_state)); + + size = size / 8; + + if (blake2b_init_key(hash, size, key, key_len) < 0) { + goto err; + } + return 0; + +err: + return -1; +} + +static void hash_update(blake2b_state* hash, uint8_t* data, size_t len) { + blake2b_update(hash, data, len); +} + +static void hash_finalize(blake2b_state* hash, uint8_t* out) { + blake2b_final(hash, out, 32); +} + +bool hash_personal_message(uint8_t* message_bytes, size_t message_byte_len, uint8_t* out_hash) { + blake2b_state inner_hash_writer; + int err = hash_init(&inner_hash_writer, 256, (uint8_t*) MESSAGE_SIGNING_KEY, 26); + if (err < 0) { + return false; + } + + hash_update(&inner_hash_writer, message_bytes, message_byte_len); + hash_finalize(&inner_hash_writer, out_hash); + + return true; +} diff --git a/src/personal_message.h b/src/personal_message.h new file mode 100644 index 0000000..d6be0dc --- /dev/null +++ b/src/personal_message.h @@ -0,0 +1,27 @@ +/***************************************************************************** + * MIT License + * + * Copyright (c) 2023 coderofstuff + * + * 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 + +bool hash_personal_message(uint8_t* message_bytes, size_t message_byte_len, uint8_t* out_hash); \ No newline at end of file diff --git a/src/sw.h b/src/sw.h index 97fb3ce..2f81e8a 100644 --- a/src/sw.h +++ b/src/sw.h @@ -88,3 +88,4 @@ #define SW_WRONG_BIP32_COIN_TYPE 0xB00A #define SW_WRONG_BIP32_TYPE 0xB00B #define SW_WRONG_BIP32_PATH_LEN 0xB00C +#define SW_MESSAGE_TOO_LONG 0xB00D diff --git a/src/types.h b/src/types.h index 6b42725..454b86e 100644 --- a/src/types.h +++ b/src/types.h @@ -40,7 +40,8 @@ typedef enum { GET_VERSION = 0x03, /// version of the application GET_APP_NAME = 0x04, /// name of the application GET_PUBLIC_KEY = 0x05, /// public key of corresponding BIP32 path - SIGN_TX = 0x06 /// sign transaction with BIP32 path + SIGN_TX = 0x06, /// sign transaction with BIP32 path + SIGN_MESSAGE = 0x07 /// sign a personal message with BIP32 path } command_e; /** @@ -74,8 +75,9 @@ typedef enum { * Enumeration with user request type. */ typedef enum { - CONFIRM_ADDRESS, /// confirm address derived from public key - CONFIRM_TRANSACTION /// confirm transaction information + CONFIRM_ADDRESS, /// confirm address derived from public key + CONFIRM_TRANSACTION, /// confirm transaction information + CONFIRM_MESSAGE /// confirm message information } request_type_e; /** @@ -98,14 +100,27 @@ typedef struct { uint8_t parsing_output_index; } transaction_ctx_t; +/** + * Structure for message signing information context. + */ +typedef struct { + size_t message_len; /// message length + uint8_t message[128]; /// message bytes + uint8_t message_hash[32]; /// message hash + uint8_t signature[MAX_DER_SIG_LEN]; /// signature of the message + uint8_t address_type; /// address type to use for bip32 path + uint32_t address_index; /// address index to use for bip32 path +} message_sign_ctx_t; + /** * Structure for global context. */ typedef struct { state_e state; /// state of the context union { - pubkey_ctx_t pk_info; /// public key context - transaction_ctx_t tx_info; /// transaction context + pubkey_ctx_t pk_info; /// public key context + transaction_ctx_t tx_info; /// transaction context + message_sign_ctx_t msg_info; /// message sign context }; request_type_e req_type; /// user request uint32_t bip32_path[MAX_BIP32_PATH]; /// BIP32 path diff --git a/src/ui/action/validate.c b/src/ui/action/validate.c index 393fcb2..1c7ec9c 100644 --- a/src/ui/action/validate.c +++ b/src/ui/action/validate.c @@ -42,7 +42,7 @@ void validate_transaction(bool choice) { if (choice) { G_context.state = STATE_APPROVED; - int error = crypto_sign_message(); + int error = crypto_sign_transaction(); if (error != 0) { G_context.state = STATE_NONE; io_send_sw(error); @@ -55,3 +55,20 @@ void validate_transaction(bool choice) { io_send_sw(SW_DENY); } } + +void validate_message(bool choice) { + if (choice) { + G_context.state = STATE_APPROVED; + + int error = crypto_sign_personal_message(); + if (error != 0) { + G_context.state = STATE_NONE; + io_send_sw(error); + } else { + helper_send_response_personal_message_sig(); + } + } else { + G_context.state = STATE_NONE; + io_send_sw(SW_DENY); + } +} \ No newline at end of file diff --git a/src/ui/action/validate.h b/src/ui/action/validate.h index 5499613..347fe88 100644 --- a/src/ui/action/validate.h +++ b/src/ui/action/validate.h @@ -42,3 +42,12 @@ void validate_pubkey(bool choice); * */ void validate_transaction(bool choice); + +/** + * Action for message information validation. + * + * @param[in] choice + * User choice (either approved or rejectd). + * + */ +void validate_message(bool choice); diff --git a/src/ui/bagl_display.c b/src/ui/bagl_display.c index d0ef16d..6e55f66 100644 --- a/src/ui/bagl_display.c +++ b/src/ui/bagl_display.c @@ -52,6 +52,7 @@ static char g_amount[30]; static char g_bip32_path[60]; static char g_address[ECDSA_ADDRESS_LEN + 6]; static char g_fees[30]; +static char g_message[MAX_MESSAGE_LEN + 6]; // Validate/Invalidate public key and go back to home static void ui_action_validate_pubkey(bool choice) { @@ -65,6 +66,12 @@ static void ui_action_validate_transaction(bool choice) { ui_menu_main(); } +// Validate/Invalidate message and go back to home +static void ui_action_validate_message(bool choice) { + validate_message(choice); + ui_menu_main(); +} + // Step with icon and text UX_STEP_NOCB(ux_display_confirm_addr_step, pn, {&C_icon_eye, "Confirm Address"}); // Step with title/text for BIP32 path @@ -221,4 +228,55 @@ int ui_display_transaction() { return 0; } +// Step with icon and text +UX_STEP_NOCB(ux_display_confirm_message_step, pn, {&C_icon_eye, "Confirm Message"}); + +// Step with title/text for message +UX_STEP_NOCB(ux_display_message_step, + bnnn_paging, + { + .title = "Message", + .text = g_message, + }); + +// FLOW to display address and BIP32 path: +// #1 screen: eye icon + "Confirm Address" +// #2 screen: display BIP32 Path +// #3 screen: display address +// #4 screen: approve button +// #5 screen: reject button +UX_FLOW(ux_display_message_flow, + &ux_display_confirm_message_step, + &ux_display_path_step, + &ux_display_message_step, + &ux_display_approve_step, + &ux_display_reject_step); + +int ui_display_message() { + if (G_context.req_type != CONFIRM_MESSAGE || G_context.state != STATE_NONE) { + G_context.state = STATE_NONE; + return io_send_sw(SW_BAD_STATE); + } + + memset(g_bip32_path, 0, sizeof(g_bip32_path)); + if (!bip32_path_format(G_context.bip32_path, + G_context.bip32_path_len, + g_bip32_path, + sizeof(g_bip32_path))) { + return io_send_sw(SW_DISPLAY_BIP32_PATH_FAIL); + } + + memset(g_message, 0, sizeof(g_message)); + snprintf(g_message, + sizeof(g_message), + "%.*s", + G_context.msg_info.message_len, + G_context.msg_info.message); + + g_validate_callback = &ui_action_validate_message; + + ux_flow_init(0, ux_display_message_flow, NULL); + return 0; +} + #endif diff --git a/src/ui/display.h b/src/ui/display.h index 61afa84..b6c38b5 100644 --- a/src/ui/display.h +++ b/src/ui/display.h @@ -45,3 +45,11 @@ int ui_display_address(void); * */ int ui_display_transaction(void); + +/** + * Display message information on the device and ask confirmation to sign. + * + * @return 0 if success, negative integer otherwise. + * + */ +int ui_display_message(void); diff --git a/src/ui/nbgl_display_message.c b/src/ui/nbgl_display_message.c new file mode 100644 index 0000000..dee6e2e --- /dev/null +++ b/src/ui/nbgl_display_message.c @@ -0,0 +1,126 @@ +/***************************************************************************** + * MIT License + * + * Copyright (c) 2023 coderofstuff + * + * 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. + *****************************************************************************/ +#ifdef HAVE_NBGL + +#pragma GCC diagnostic ignored "-Wformat-invalid-specifier" // snprintf +#pragma GCC diagnostic ignored "-Wformat-extra-args" // snprintf + +#include // bool +#include // memset + +#include "os.h" +#include "glyphs.h" +#include "nbgl_use_case.h" + +#include "display.h" +#include "constants.h" +#include "../globals.h" +#include "io.h" +#include "../sw.h" +#include "../address.h" +#include "action/validate.h" +#include "../types.h" +#include "../transaction/types.h" +#include "bip32.h" +#include "../common/format.h" +#include "../menu.h" + +static char g_message[MAX_MESSAGE_LEN]; +static char g_bip32_path[60]; + +static nbgl_layoutTagValue_t pairs[2]; +static nbgl_layoutTagValueList_t pairList; +static nbgl_pageInfoLongPress_t infoLongPress; + +static void confirm_message_rejection(void) { + // display a status page and go back to main + validate_message(false); + nbgl_useCaseStatus("Message signing\ncancelled", false, ui_menu_main); +} + +static void confirm_message_approval(void) { + // display a success status page and go back to main + validate_message(true); + nbgl_useCaseStatus("MESSAGE\nSIGNED", true, ui_menu_main); +} + +static void review_message_choice(bool confirm) { + if (confirm) { + confirm_message_approval(); + } else { + confirm_message_rejection(); + } +} + +static void continue_message_review(void) { + // Fill pairs + pairs[0].item = "BIP32 Path"; + pairs[0].value = g_bip32_path; + pairs[1].item = "Message"; + pairs[1].value = g_message; + + // Setup list + pairList.nbMaxLinesForValue = 0; + pairList.nbPairs = 2; + pairList.pairs = pairs; + + // Info long press + infoLongPress.icon = &C_stax_app_kaspa_64px; + infoLongPress.text = "Sign message"; + infoLongPress.longPressText = "Hold to sign"; + + nbgl_useCaseStaticReview(&pairList, &infoLongPress, "Reject message", review_message_choice); +} + +int ui_display_message() { + if (G_context.req_type != CONFIRM_MESSAGE || G_context.state != STATE_NONE) { + G_context.state = STATE_NONE; + return io_send_sw(SW_BAD_STATE); + } + + memset(g_bip32_path, 0, sizeof(g_bip32_path)); + if (!bip32_path_format(G_context.bip32_path, + G_context.bip32_path_len, + g_bip32_path, + sizeof(g_bip32_path))) { + return io_send_sw(SW_DISPLAY_BIP32_PATH_FAIL); + } + + memset(g_message, 0, sizeof(g_message)); + snprintf(g_message, + sizeof(g_message), + "%.*s", + G_context.msg_info.message_len, + G_context.msg_info.message); + + nbgl_useCaseReviewStart(&C_stax_app_kaspa_64px, + "Sign Message", + NULL, + "Cancel", + continue_message_review, + confirm_message_rejection); + return 0; +} + +#endif diff --git a/tests/application_client/kaspa_command_sender.py b/tests/application_client/kaspa_command_sender.py index de9f77d..74b4530 100644 --- a/tests/application_client/kaspa_command_sender.py +++ b/tests/application_client/kaspa_command_sender.py @@ -6,6 +6,7 @@ from ragger.bip import pack_derivation_path from .kaspa_transaction import Transaction +from .kaspa_message import PersonalMessage MAX_APDU_LEN: int = 255 @@ -34,6 +35,7 @@ class InsType(IntEnum): GET_APP_NAME = 0x04 GET_PUBLIC_KEY = 0x05 SIGN_TX = 0x06 + SIGN_MESSAGE = 0x07 class Errors(IntEnum): SW_DENY = 0x6985 @@ -54,13 +56,11 @@ class Errors(IntEnum): SW_WRONG_BIP32_COIN_TYPE = 0xB00A SW_WRONG_BIP32_TYPE = 0xB00B SW_WRONG_BIP32_PATH_LEN = 0xB00C - - + SW_MESSAGE_TOO_LONG = 0xB00D def split_message(message: bytes, max_size: int) -> List[bytes]: return [message[x:x + max_size] for x in range(0, len(message), max_size)] - class KaspaCommandSender: def __init__(self, backend: BackendInterface) -> None: self.backend = backend @@ -141,6 +141,15 @@ def sign_tx(self, transaction: Transaction) -> Generator[None, None, None]: yield response + @contextmanager + def sign_message(self, message_data: PersonalMessage) -> Generator[None, None, None]: + with self.backend.exchange_async(cla=CLA, + ins=InsType.SIGN_MESSAGE, + p1=P1.P1_CONFIRM, + p2=P2.P2_LAST, + data=message_data.serialize()) as response: + yield response + def get_next_signature(self) -> RAPDU: return self.backend.exchange(cla=CLA, ins=InsType.SIGN_TX, diff --git a/tests/application_client/kaspa_message.py b/tests/application_client/kaspa_message.py new file mode 100644 index 0000000..4fdf59f --- /dev/null +++ b/tests/application_client/kaspa_message.py @@ -0,0 +1,26 @@ +from hashlib import blake2b + +def hash_init() -> blake2b: + return blake2b(digest_size=32, key=bytes("PersonalMessageSigningHash", "ascii")) + +class PersonalMessage: + def __init__(self, + address_type: int, + address_index: int, + message: str): + self.address_type: int = address_type # 1 byte + self.address_index:int = address_index # 4 bytes + self.message: bytes = bytes(message, 'utf8') # var + + def serialize(self) -> bytes: + return b"".join([ + self.address_type.to_bytes(1, byteorder="big"), + self.address_index.to_bytes(4, byteorder="big"), + len(self.message).to_bytes(1, byteorder="big"), + self.message + ]) + + def to_hash(self) -> bytes: + outer_hash = hash_init() + outer_hash.update(self.message) + return outer_hash.digest() diff --git a/tests/application_client/kaspa_response_unpacker.py b/tests/application_client/kaspa_response_unpacker.py index 9f586ef..a46f047 100644 --- a/tests/application_client/kaspa_response_unpacker.py +++ b/tests/application_client/kaspa_response_unpacker.py @@ -62,6 +62,8 @@ def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, by # input_index (1) # der_sig_len (1) # der_sig (64) +# sighash_len (1) +# sighash (32) def unpack_sign_tx_response(response: bytes) -> Tuple[int, int, int, bytes, int, bytes]: response, has_more = pop_sized_buf_from_buffer(response, 1) response, input_index = pop_sized_buf_from_buffer(response, 1) @@ -76,3 +78,19 @@ def unpack_sign_tx_response(response: bytes) -> Tuple[int, int, int, bytes, int, der_sig, \ sighash_len, \ sighash + +# Unpack from response: +# response = der_sig_len (1) +# der_sig (64) +# mhash_len (1) +# mhash (32) +def unpack_sign_message_response(response: bytes) -> Tuple[int, bytes, int, bytes]: + response, der_sig_len, der_sig = pop_size_prefixed_buf_from_buf(response) + response, mhash_len, mhash = pop_size_prefixed_buf_from_buf(response) + + assert len(response) == 0 + + return der_sig_len, \ + der_sig, \ + mhash_len, \ + mhash diff --git a/tests/snapshots/nanos/test_sign_message_kanji/00000.png b/tests/snapshots/nanos/test_sign_message_kanji/00000.png new file mode 100644 index 0000000..f81b25d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_kanji/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_message_kanji/00001.png b/tests/snapshots/nanos/test_sign_message_kanji/00001.png new file mode 100644 index 0000000..17e4424 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_kanji/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_message_kanji/00002.png b/tests/snapshots/nanos/test_sign_message_kanji/00002.png new file mode 100644 index 0000000..82a806e Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_kanji/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_message_kanji/00003.png b/tests/snapshots/nanos/test_sign_message_kanji/00003.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_kanji/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_message_kanji/00004.png b/tests/snapshots/nanos/test_sign_message_kanji/00004.png new file mode 100644 index 0000000..9ab6248 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_kanji/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_message_simple/00000.png b/tests/snapshots/nanos/test_sign_message_simple/00000.png new file mode 100644 index 0000000..f81b25d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_simple/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_message_simple/00001.png b/tests/snapshots/nanos/test_sign_message_simple/00001.png new file mode 100644 index 0000000..ef269c9 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_simple/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_message_simple/00002.png b/tests/snapshots/nanos/test_sign_message_simple/00002.png new file mode 100644 index 0000000..0964ed8 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_simple/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_message_simple/00003.png b/tests/snapshots/nanos/test_sign_message_simple/00003.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_simple/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_message_simple/00004.png b/tests/snapshots/nanos/test_sign_message_simple/00004.png new file mode 100644 index 0000000..9ab6248 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_message_simple/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_kanji/00000.png b/tests/snapshots/nanosp/test_sign_message_kanji/00000.png new file mode 100644 index 0000000..a95fdd2 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_kanji/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_kanji/00001.png b/tests/snapshots/nanosp/test_sign_message_kanji/00001.png new file mode 100644 index 0000000..625a940 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_kanji/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_kanji/00002.png b/tests/snapshots/nanosp/test_sign_message_kanji/00002.png new file mode 100644 index 0000000..79c9c7d Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_kanji/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_kanji/00003.png b/tests/snapshots/nanosp/test_sign_message_kanji/00003.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_kanji/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_kanji/00004.png b/tests/snapshots/nanosp/test_sign_message_kanji/00004.png new file mode 100644 index 0000000..63b43ce Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_kanji/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_simple/00000.png b/tests/snapshots/nanosp/test_sign_message_simple/00000.png new file mode 100644 index 0000000..a95fdd2 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_simple/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_simple/00001.png b/tests/snapshots/nanosp/test_sign_message_simple/00001.png new file mode 100644 index 0000000..103913f Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_simple/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_simple/00002.png b/tests/snapshots/nanosp/test_sign_message_simple/00002.png new file mode 100644 index 0000000..6df9c9d Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_simple/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_simple/00003.png b/tests/snapshots/nanosp/test_sign_message_simple/00003.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_simple/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_message_simple/00004.png b/tests/snapshots/nanosp/test_sign_message_simple/00004.png new file mode 100644 index 0000000..63b43ce Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_message_simple/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_message_kanji/00000.png b/tests/snapshots/nanox/test_sign_message_kanji/00000.png new file mode 100644 index 0000000..a95fdd2 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_kanji/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_message_kanji/00001.png b/tests/snapshots/nanox/test_sign_message_kanji/00001.png new file mode 100644 index 0000000..625a940 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_kanji/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_message_kanji/00002.png b/tests/snapshots/nanox/test_sign_message_kanji/00002.png new file mode 100644 index 0000000..79c9c7d Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_kanji/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_message_kanji/00003.png b/tests/snapshots/nanox/test_sign_message_kanji/00003.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_kanji/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_message_kanji/00004.png b/tests/snapshots/nanox/test_sign_message_kanji/00004.png new file mode 100644 index 0000000..8ce05d9 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_kanji/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_message_simple/00000.png b/tests/snapshots/nanox/test_sign_message_simple/00000.png new file mode 100644 index 0000000..a95fdd2 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_simple/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_message_simple/00001.png b/tests/snapshots/nanox/test_sign_message_simple/00001.png new file mode 100644 index 0000000..103913f Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_simple/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_message_simple/00002.png b/tests/snapshots/nanox/test_sign_message_simple/00002.png new file mode 100644 index 0000000..6df9c9d Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_simple/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_message_simple/00003.png b/tests/snapshots/nanox/test_sign_message_simple/00003.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_simple/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_message_simple/00004.png b/tests/snapshots/nanox/test_sign_message_simple/00004.png new file mode 100644 index 0000000..8ce05d9 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_message_simple/00004.png differ diff --git a/tests/snapshots/stax/test_sign_message_kanji/00000.png b/tests/snapshots/stax/test_sign_message_kanji/00000.png new file mode 100644 index 0000000..ef8c0f1 Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_kanji/00000.png differ diff --git a/tests/snapshots/stax/test_sign_message_kanji/00001.png b/tests/snapshots/stax/test_sign_message_kanji/00001.png new file mode 100644 index 0000000..ea783c2 Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_kanji/00001.png differ diff --git a/tests/snapshots/stax/test_sign_message_kanji/00002.png b/tests/snapshots/stax/test_sign_message_kanji/00002.png new file mode 100644 index 0000000..c428ef3 Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_kanji/00002.png differ diff --git a/tests/snapshots/stax/test_sign_message_kanji/00003.png b/tests/snapshots/stax/test_sign_message_kanji/00003.png new file mode 100644 index 0000000..c1a8fd4 Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_kanji/00003.png differ diff --git a/tests/snapshots/stax/test_sign_message_kanji/00004.png b/tests/snapshots/stax/test_sign_message_kanji/00004.png new file mode 100644 index 0000000..ad6b72e Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_kanji/00004.png differ diff --git a/tests/snapshots/stax/test_sign_message_simple/00000.png b/tests/snapshots/stax/test_sign_message_simple/00000.png new file mode 100644 index 0000000..ef8c0f1 Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_simple/00000.png differ diff --git a/tests/snapshots/stax/test_sign_message_simple/00001.png b/tests/snapshots/stax/test_sign_message_simple/00001.png new file mode 100644 index 0000000..327244e Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_simple/00001.png differ diff --git a/tests/snapshots/stax/test_sign_message_simple/00002.png b/tests/snapshots/stax/test_sign_message_simple/00002.png new file mode 100644 index 0000000..c428ef3 Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_simple/00002.png differ diff --git a/tests/snapshots/stax/test_sign_message_simple/00003.png b/tests/snapshots/stax/test_sign_message_simple/00003.png new file mode 100644 index 0000000..c1a8fd4 Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_simple/00003.png differ diff --git a/tests/snapshots/stax/test_sign_message_simple/00004.png b/tests/snapshots/stax/test_sign_message_simple/00004.png new file mode 100644 index 0000000..ad6b72e Binary files /dev/null and b/tests/snapshots/stax/test_sign_message_simple/00004.png differ diff --git a/tests/test_sign_cmd.py b/tests/test_sign_cmd.py index 0c7dd6d..492e3bf 100644 --- a/tests/test_sign_cmd.py +++ b/tests/test_sign_cmd.py @@ -64,7 +64,7 @@ def test_sign_tx_simple(firmware, backend, navigator, test_name): response = client.get_async_response().data _, _, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(0) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) def test_sign_tx_simple_ecdsa(firmware, backend, navigator, test_name): # Use the app interface instead of raw interface @@ -120,7 +120,7 @@ def test_sign_tx_simple_ecdsa(firmware, backend, navigator, test_name): response = client.get_async_response().data _, _, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(0) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) def test_sign_tx_p2sh(firmware, backend, navigator, test_name): @@ -177,7 +177,7 @@ def test_sign_tx_p2sh(firmware, backend, navigator, test_name): response = client.get_async_response().data _, _, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(0) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) def test_sign_tx_simple_sendint(firmware, backend, navigator, test_name): # Use the app interface instead of raw interface @@ -233,7 +233,7 @@ def test_sign_tx_simple_sendint(firmware, backend, navigator, test_name): response = client.get_async_response().data _, _, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(0) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) def test_sign_tx_simple_sendmaxu64(firmware, backend, navigator, test_name): # Use the app interface instead of raw interface @@ -289,7 +289,7 @@ def test_sign_tx_simple_sendmaxu64(firmware, backend, navigator, test_name): response = client.get_async_response().data _, _, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(0) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) def test_sign_tx_simple_change_idx1(firmware, backend, navigator, test_name): # Use the app interface instead of raw interface @@ -358,7 +358,7 @@ def test_sign_tx_simple_change_idx1(firmware, backend, navigator, test_name): response = client.get_async_response().data _, _, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(0) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) def test_sign_tx_with_change(firmware, backend, navigator, test_name): # Use the app interface instead of raw interface @@ -423,7 +423,7 @@ def test_sign_tx_with_change(firmware, backend, navigator, test_name): response = client.get_async_response().data _, _, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(0) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) def test_sign_tx_with_invalid_change(backend): backend.raise_policy = RaisePolicy.RAISE_NOTHING @@ -666,7 +666,7 @@ def test_sign_tx_max(firmware, backend, navigator, test_name): response = client.get_async_response().data has_more, input_index, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(input_index) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) while has_more > 0: idx = idx + 1 @@ -677,7 +677,7 @@ def test_sign_tx_max(firmware, backend, navigator, test_name): response = client.get_next_signature().data has_more, input_index, _, der_sig, _, sighash = unpack_sign_tx_response(response) assert transaction.get_sighash(input_index) == sighash - assert check_signature_validity(public_key, der_sig, sighash, transaction) + assert check_signature_validity(public_key, der_sig, sighash) # Transaction signature refused test # The test will ask for a transaction signature that will be refused on screen diff --git a/tests/test_sign_personal_message_cmd.py b/tests/test_sign_personal_message_cmd.py new file mode 100644 index 0000000..34b53fd --- /dev/null +++ b/tests/test_sign_personal_message_cmd.py @@ -0,0 +1,111 @@ +from application_client.kaspa_command_sender import KaspaCommandSender, Errors, InsType, P1, P2 +from application_client.kaspa_message import PersonalMessage +from application_client.kaspa_response_unpacker import unpack_get_public_key_response, unpack_sign_message_response +from ragger.backend import RaisePolicy +from ragger.navigator import NavInsID +from utils import ROOT_SCREENSHOT_PATH, check_signature_validity + +# In this tests we check the behavior of the device when asked to sign a transaction + + +# In this test se send to the device a transaction to sign and validate it on screen +# We will ensure that the displayed information is correct by using screenshots comparison +def test_sign_message_simple(firmware, backend, navigator, test_name): + # Use the app interface instead of raw interface + client = KaspaCommandSender(backend) + # The path used for this entire test + path: str = "m/44'/111111'/0'/1/5" + + # First we need to get the public key of the device in order to build the transaction + rapdu = client.get_public_key(path=path) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + address_type = 1 + address_index = 5 + message = "Hello Kaspa!" + + message_data = PersonalMessage(address_type, address_index, message) + + # Send the sign device instruction. + # As it requires on-screen validation, the function is asynchronous. + # It will yield the result when the navigation is done + with client.sign_message(message_data=message_data): + # Validate the on-screen request by performing the navigation appropriate for this device + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name) + else: + navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_TAP, + [NavInsID.USE_CASE_REVIEW_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name) + + # The device as yielded the result, parse it and ensure that the signature is correct + response = client.get_async_response().data + _, der_sig, _, message_hash = unpack_sign_message_response(response) + + assert message_hash == message_data.to_hash() + assert check_signature_validity(public_key, der_sig, message_hash) + +def test_sign_message_kanji(firmware, backend, navigator, test_name): + # Use the app interface instead of raw interface + client = KaspaCommandSender(backend) + # The path used for this entire test + path: str = "m/44'/111111'/0'/1/3" + + # First we need to get the public key of the device in order to build the transaction + rapdu = client.get_public_key(path=path) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + address_type = 1 + address_index = 3 + message = "こんにちは世界" + + message_data = PersonalMessage(address_type, address_index, message) + + # Send the sign device instruction. + # As it requires on-screen validation, the function is asynchronous. + # It will yield the result when the navigation is done + with client.sign_message(message_data=message_data): + # Validate the on-screen request by performing the navigation appropriate for this device + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name) + else: + navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_TAP, + [NavInsID.USE_CASE_REVIEW_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name) + + # The device as yielded the result, parse it and ensure that the signature is correct + response = client.get_async_response().data + _, der_sig, _, message_hash = unpack_sign_message_response(response) + + assert message_hash == message_data.to_hash() + assert check_signature_validity(public_key, der_sig, message_hash) + +def test_sign_message_too_long(firmware, backend, navigator, test_name): + backend.raise_policy = RaisePolicy.RAISE_NOTHING + # Use the app interface instead of raw interface + client = KaspaCommandSender(backend) + + address_type = 1 + address_index = 4 + message = '''Lorem ipsum dolor sit amet. Aut omnis amet id voluptatem eligendi sit accusantium dolorem 33 corrupti necessitatibus hic consequatur quod et maiores alias non molestias suscipit? Est voluptatem magni qui odit eius est eveniet cupiditate id eius quae''' + + message_data = PersonalMessage(address_type, address_index, message) + + last_response = client.send_raw_apdu(InsType.SIGN_MESSAGE, p1=P1.P1_INPUTS, p2=P2.P2_LAST, data=message_data.serialize()) + + assert last_response.status == Errors.SW_MESSAGE_TOO_LONG + \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py index 76b26f9..8a6b511 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,12 +2,10 @@ from secp256k1 import PublicKey -from application_client.kaspa_transaction import Transaction - ROOT_SCREENSHOT_PATH = Path(__file__).parent.resolve() # Check if a signature of a given message is valid -def check_signature_validity(public_key: bytes, signature: bytes, sighash: bytes, transaction: Transaction) -> bool: +def check_signature_validity(public_key: bytes, signature: bytes, sighash: bytes) -> bool: pkey = PublicKey(public_key, True) return pkey.schnorr_verify(sighash, signature, None, True) diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt index 9299687..0842ec4 100644 --- a/unit-tests/CMakeLists.txt +++ b/unit-tests/CMakeLists.txt @@ -33,7 +33,7 @@ if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt. ") endif() -add_compile_definitions(TEST HAVE_HASH HAVE_BLAKE2 HAVE_ECC USB_SEGMENT_SIZE=64 MAX_INPUT_COUNT=15 IO_SEPROXYHAL_BUFFER_SIZE_B=128) +add_compile_definitions(TEST HAVE_HASH HAVE_BLAKE2 HAVE_ECC USB_SEGMENT_SIZE=64 MAX_INPUT_COUNT=15 MAX_MESSAGE_LEN=200 IO_SEPROXYHAL_BUFFER_SIZE_B=128) include_directories(../src) # include_directories(mock_includes) @@ -47,6 +47,7 @@ include_directories(/opt/ledger-secure-sdk/lib_standard_app/) add_executable(test_address test_address.c) add_executable(test_format test_format.c) add_executable(test_sighash test_sighash.c) +add_executable(test_personal_message test_personal_message.c) add_executable(test_apdu_parser test_apdu_parser.c) add_executable(test_tx_parser test_tx_parser.c) add_executable(test_tx_utils test_tx_utils.c) @@ -58,6 +59,7 @@ add_library(bip32 SHARED /opt/ledger-secure-sdk/lib_standard_app/bip32.c) add_library(buffer SHARED /opt/ledger-secure-sdk/lib_standard_app/buffer.c) add_library(read SHARED /opt/ledger-secure-sdk/lib_standard_app/read.c) add_library(sighash SHARED ../src/sighash.c) +add_library(personal_message SHARED ../src/personal_message.c) add_library(write SHARED /opt/ledger-secure-sdk/lib_standard_app/write.c) add_library(format SHARED ../src/common/format.c) add_library(apdu_parser SHARED ../src/apdu/parser.c) @@ -69,6 +71,7 @@ add_library(varint SHARED /opt/ledger-secure-sdk/lib_standard_app/varint.c) target_link_libraries(test_address PUBLIC cmocka gcov address cashaddr) target_link_libraries(test_format PUBLIC cmocka gcov format) target_link_libraries(test_sighash PUBLIC cmocka gcov sighash blake2b write) +target_link_libraries(test_personal_message PUBLIC cmocka gcov personal_message blake2b write) target_link_libraries(test_apdu_parser PUBLIC cmocka gcov apdu_parser) target_link_libraries(test_tx_parser PUBLIC transaction_deserialize @@ -91,6 +94,7 @@ target_link_libraries(test_tx_utils PUBLIC add_test(test_address test_address) add_test(test_format test_format) add_test(test_sighash test_sighash) +add_test(test_personal_message test_personal_message) add_test(test_apdu_parser test_apdu_parser) add_test(test_tx_parser test_tx_parser) add_test(test_tx_utils test_tx_utils) diff --git a/unit-tests/test_personal_message.c b/unit-tests/test_personal_message.c new file mode 100644 index 0000000..7616863 --- /dev/null +++ b/unit-tests/test_personal_message.c @@ -0,0 +1,107 @@ +/***************************************************************************** + * MIT License + * + * Copyright (c) 2023 coderofstuff + * + * 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 "./import/blake2-impl.h" +#include "./import/blake2b.h" +#include "personal_message.h" + +/* Start hacks */ +void os_longjmp(unsigned int exception) {} + +struct cx_xblake_s { + cx_blake2b_t blake2b; + uint64_t m[16]; + uint64_t v[16]; + uint8_t buffer[BLAKE2B_OUTBYTES]; + uint8_t block1[BLAKE2B_BLOCKBYTES]; +} ; +typedef struct cx_xblake_s cx_xblake_t; + +union cx_u { + cx_xblake_t blake; +}; +union cx_u G_cx; + +uint8_t* input_public_key; + +/* End hacks */ + +static void test_hash_personal_message_vector0(void **state) { + char message[] = "Hello Kaspa!"; // 12 chars + + uint8_t out_hash[32] = {0}; + + bool result = hash_personal_message((uint8_t*) message, 12, out_hash); + + assert_true(result); + + uint8_t res[32] = {0x2E, 0x55, 0xDE, 0xDA, 0x4A, 0x52, 0x24, 0x20, + 0x8D, 0xBD, 0x4D, 0x93, 0xCF, 0xE5, 0xAA, 0x22, + 0xD9, 0x45, 0xEA, 0xA6, 0x31, 0x72, 0xE3, 0x29, + 0xC4, 0x9A, 0xFE, 0xD6, 0x2F, 0x0E, 0x15, 0x10}; + + assert_memory_equal(out_hash, res, 32); +} + +// static void test_sign_message_vector1(void **state) { +// uint8_t private_key_data[32] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03}; + +// char message[] = "Hello Kaspa!"; // 12 chars + +// uint8_t out_hash[64] = {0}; + +// bool result = hash_personal_message(private_key_data, (uint8_t*) message, 12, out_hash); + +// assert_true(result); + +// uint8_t res[64] = {0x40, 0xB9, 0xBB, 0x2B, 0xE0, 0xAE, 0x02, 0x60, +// 0x72, 0x79, 0xED, 0xA6, 0x40, 0x15, 0xA8, 0xD8, +// 0x6E, 0x37, 0x63, 0x27, 0x91, 0x70, 0x34, 0x0B, +// 0x82, 0x43, 0xF7, 0xCE, 0x53, 0x44, 0xD7, 0x7A, +// 0xFF, 0x11, 0x91, 0x59, 0x8B, 0xAF, 0x2F, 0xD2, +// 0x61, 0x49, 0xCA, 0xC3, 0xB4, 0xB1, 0x2C, 0x2C, +// 0x43, 0x32, 0x61, 0xC0, 0x08, 0x34, 0xDB, 0x60, +// 0x98, 0xCB, 0x17, 0x2A, 0xA4, 0x8E, 0xF5, 0x22}; + +// assert_memory_equal(out_hash, res, 64); +// } + +//612d56e633ee5da1caa4563c6ace0c98d3549ad4e3d2b1f1ea6810e6c34047bd + + +int main() { + const struct CMUnitTest tests[] = {cmocka_unit_test(test_hash_personal_message_vector0)}; + + return cmocka_run_group_tests(tests, NULL, NULL); +}