diff --git a/apps/constellation_app/constellation_context.h b/apps/constellation_app/constellation_context.h index 08ab574dd..4efa19302 100644 --- a/apps/constellation_app/constellation_context.h +++ b/apps/constellation_app/constellation_context.h @@ -28,9 +28,12 @@ #define CONSTELLATION_ACCOUNT_INDEX 0x80000000 // 0' #define CONSTELLATION_CHANGE_INDEX 0x00000000 // 0 -#define CONSTELLATION_PUB_KEY_SIZE 33 -#define CONSTELLATION_UNCOMPRESSED_PUB_KEY_SIZE 65 -#define CONSTELLATION_ACCOUNT_ADDRESS_LENGTH 40 +#define CONSTELLATION_PUB_KEY_SIZE 65 +#define CONSTELLATION_ACCOUNT_ADDRESS_SIZE 40 +#define PKCS_PREFIX_SIZE 23 +#define PKCS_PREFIXED_PUBKEY_SIZE PKCS_PREFIX_SIZE + CONSTELLATION_PUB_KEY_SIZE +#define SHA256_DIGEST_SIZE 32 +#define BS58_ENCODED_SIZE 45 /***************************************************************************** * TYPEDEFS diff --git a/apps/constellation_app/constellation_main.c b/apps/constellation_app/constellation_main.c index 4c5a331ae..bec5ce24e 100644 --- a/apps/constellation_app/constellation_main.c +++ b/apps/constellation_app/constellation_main.c @@ -130,7 +130,7 @@ void constellation_main(usb_event_t usb_evt, switch ((uint8_t)query.which_request) { case CONSTELLATION_QUERY_GET_PUBLIC_KEYS_TAG: case CONSTELLATION_QUERY_GET_USER_VERIFIED_PUBLIC_KEY_TAG: { - // constellation_get_pub_keys(&query); + constellation_get_pub_keys(&query); break; } case CONSTELLATION_QUERY_SIGN_TXN_TAG: { diff --git a/apps/constellation_app/constellation_pub_key.c b/apps/constellation_app/constellation_pub_key.c new file mode 100644 index 000000000..3d745d8bd --- /dev/null +++ b/apps/constellation_app/constellation_pub_key.c @@ -0,0 +1,516 @@ +/** + * @file constellation_pub_key.c + * @author Cypherock X1 Team + * @brief Generates public key for CONSTELLATION derivations. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + *
You may obtain a copy of license at https://mitcc.org/ + * + ****************************************************************************** + * @attention + * + * (c) Copyright 2024 by HODL TECH PTE 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. + * + * + * "Commons Clause" License Condition v1.0 + * + * The Software is provided to you by the Licensor under the License, + * as defined below, subject to the following condition. + * + * Without limiting other conditions in the License, the grant of + * rights under the License will not include, and the License does not + * grant to you, the right to Sell the Software. + * + * For purposes of the foregoing, "Sell" means practicing any or all + * of the rights granted to you under the License to provide to third + * parties, for a fee or other consideration (including without + * limitation fees for hosting or consulting/ support services related + * to the Software), a product or service whose value derives, entirely + * or substantially, from the functionality of the Software. Any license + * notice or attribution required by the License must also include + * this Commons Clause License Condition notice. + * + * Software: All X1Wallet associated files. + * License: MIT + * Licensor: HODL TECH PTE LTD + * + ****************************************************************************** + */ + +/***************************************************************************** + * INCLUDES + *****************************************************************************/ + +#include +#include + +#include "base58.h" +#include "bip32.h" +#include "coin_utils.h" +#include "constellation_api.h" +#include "constellation_context.h" +#include "constellation_helpers.h" +#include "constellation_priv.h" +#include "curves.h" +#include "ecdsa.h" +#include "hasher.h" +#include "reconstruct_wallet_flow.h" +#include "secp256k1.h" +#include "sha2.h" +#include "status_api.h" +#include "ui_core_confirm.h" +#include "ui_screens.h" +#include "wallet_list.h" + +/***************************************************************************** + * EXTERN VARIABLES + *****************************************************************************/ + +/***************************************************************************** + * PRIVATE MACROS AND DEFINES + *****************************************************************************/ + +/***************************************************************************** + * PRIVATE TYPEDEFS + *****************************************************************************/ + +/***************************************************************************** + * STATIC FUNCTION PROTOTYPES + *****************************************************************************/ + +/** + * @brief Checks if the provided query contains expected request. + * @details The function performs the check on the request type and if the check + * fails, then it will send an error to the host bitcoin app and return false. + * + * @param query Reference to an instance of constellation_query_t containing + * query received from host app + * @param which_request The expected request type enum + * + * @return bool Indicating if the check succeeded or failed + * @retval true If the query contains the expected request + * @retval false If the query does not contain the expected request + */ +static bool check_which_request(const constellation_query_t *query, + pb_size_t which_request); + +/** + * @brief Validates the derivation paths received in the request from host + * @details The function validates each path index in the request. If any + * invalid index is detected, the function will send an error to the host and + * return false. + * + * @param req Reference to an instance of + * constellation_get_public_keys_intiate_request_t + * @param which_request The type of request received from the host. + * @return bool Indicating if the verification passed or failed + * @retval true If the derivation path entries are valid + * @retval false If any of the derivation path entries are invalid + */ +static bool validate_request( + const constellation_get_public_keys_intiate_request_t *req, + const pb_size_t which_request); + +/** + * @brief Fills the list of public keys corresponding to the provided list of + * derivation paths in the buffer + * @details The function expects the size of list for derivation paths and + * location for storing derived public keys to be a match with provided count. + * + * @param path Reference to the list of + * constellation_get_public_keys_derivation_path_t + * @param seed Reference to a const array containing the seed + * @param public_key Reference to the location to store all the public keys to + * be derived + * @param count Number of derivation paths in the list and consequently, + * sufficient space in memory for storing derived public keys. + * + * @retval true If all the requested public keys were derived successfully + * @retval false If there is any issue occurred during the key derivation + */ +static bool fill_public_keys( + const constellation_get_public_keys_derivation_path_t *path, + const uint8_t *seed, + uint8_t public_key_list[][CONSTELLATION_PUB_KEY_SIZE], + pb_size_t count); + +/** + * @brief The function sends public keys for the requested batch + * @details The function determines the batch size from the static struct + * member declaration of nanopb options. The function batches the result based + * on the definition and sends the result. The function expects that the entire + * list of public keys requested is already derived and provided to this + * function as pubkey_list. The function will return false if either the query + * was wrong or a P0 event is occurred. In case of wrong query, the function + * also sends an error to the host app. + * + * @param query Reference to an instance of constellation_query_t + * @param pubkey_list Reference to list of derived public key to be sent to the + * host + * @param count Number of public keys entries in the list of public keys + * @param which_request The type of request to be expected from the host + * @param which_response The type of response to be sent to the host + * + * @return bool Indicating if the public keys was exported completely to the + * host + * @retval true If all the requested public keys were exported to the host app + * @retval false If the export was interrupted by a P0 event or an invalid query + * was received from the host app. + */ +static bool send_public_keys( + constellation_query_t *query, + const uint8_t pubkey_list[][CONSTELLATION_PUB_KEY_SIZE], + const pb_size_t count, + const pb_size_t which_request, + const pb_size_t which_response); + +/** + * @details The function provides an ED25519 public key for CONSTELLATION. It + * accepts NULL for output parameter and handles accordingly. The function also + * manages all the terminal errors during derivation/encoding, in which case it + * will return false and send a relevant error to the host closing the + * request-response pair All the errors/invalid cases are conveyed to the host + * as unknown_error = 1 because we expect the data validation was success. + * TODO: Make this a common utility function + * + * @param seed Reference to the wallet seed generated + * @param path Derivation path of the node to be derived + * @param path_length Expected length of the provided derivation path + * @param public_key Storage location for raw uncompressed public key + * + * @retval false If derivation failed + */ +static bool get_public_key(const uint8_t *seed, + const uint32_t *path, + uint32_t path_length, + uint8_t *public_key); + +/** + * @brief Helper function to take user consent before exporting public keys to + * the host. Uses an appropriate message template based on the query request + * received from the host. + * + * @param which_request The type of request received from host + * @param wallet_name The name of the wallet on which the request needs to be + * performed + * @return true If the user accepted the request + * @return false If the user rejected or any P0 event occurred during the + * confirmation. + */ +static bool get_user_consent(const pb_size_t which_request, + const char *wallet_name); + +/***************************************************************************** + * STATIC VARIABLES + *****************************************************************************/ + +/// Ref: +/// https://github.com/StardustCollective/dag4.js/blob/main/packages/dag4-keystore/src/key-store.ts#L39 +static const uint8_t PKCS_PREFIX[PKCS_PREFIX_SIZE] = { + 0x30, 0x56, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, + 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, 0x03, 0x42, 0x00}; + +static bool check_which_request(const constellation_query_t *query, + pb_size_t which_request) { + if (which_request != query->get_public_keys.which_request) { + constellation_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, + ERROR_DATA_FLOW_INVALID_REQUEST); + return false; + } + + return true; +} + +static bool validate_request( + const constellation_get_public_keys_intiate_request_t *req, + const pb_size_t which_request) { + bool status = true; + const pb_size_t count = req->derivation_paths_count; + + if (0 == count) { + // request does not have any derivation paths, invalid request + constellation_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, + ERROR_DATA_FLOW_INVALID_DATA); + status = false; + } + + if (CONSTELLATION_QUERY_GET_USER_VERIFIED_PUBLIC_KEY_TAG == which_request && + 1 < count) { + // `CONSTELLATION_QUERY_GET_USER_VERIFIED_PUBLIC_KEY_TAG` request contains + // more than one derivation path which is not expected + constellation_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, + ERROR_DATA_FLOW_INVALID_DATA); + status = false; + } + + const constellation_get_public_keys_derivation_path_t *path = NULL; + + for (pb_size_t index = 0; index < count; index++) { + path = &req->derivation_paths[index]; + if (!constellation_derivation_path_guard(path->path, path->path_count)) { + constellation_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, + ERROR_DATA_FLOW_INVALID_DATA); + status = false; + break; + } + } + + return status; +} + +static bool get_public_key(const uint8_t *seed, + const uint32_t *path, + uint32_t path_length, + uint8_t *public_key) { + HDNode node = {0}; + + if (!derive_hdnode_from_path( + path, path_length, SECP256K1_NAME, seed, &node)) { + // send unknown error; unknown failure reason + constellation_send_error(ERROR_COMMON_ERROR_UNKNOWN_ERROR_TAG, 1); + memzero(&node, sizeof(HDNode)); + return false; + } + + if (NULL != public_key) { + ecdsa_uncompress_pubkey(&secp256k1, node.public_key, public_key); + } + + memzero(&node, sizeof(HDNode)); + return true; +} + +static bool fill_public_keys( + const constellation_get_public_keys_derivation_path_t *path, + const uint8_t *seed, + uint8_t public_key_list[][CONSTELLATION_PUB_KEY_SIZE], + pb_size_t count) { + for (pb_size_t index = 0; index < count; index++) { + const constellation_get_public_keys_derivation_path_t *current = + &path[index]; + if (!get_public_key( + seed, current->path, current->path_count, public_key_list[index])) { + return false; + } + } + + return true; +} + +static bool send_public_keys( + constellation_query_t *query, + const uint8_t pubkey_list[][CONSTELLATION_PUB_KEY_SIZE], + const pb_size_t count, + const pb_size_t which_request, + const pb_size_t which_response) { + constellation_result_t response = init_constellation_result(which_response); + constellation_get_public_keys_result_response_t *result = + &response.get_public_keys.result; + size_t batch_limit = sizeof(response.get_public_keys.result.public_keys) / + CONSTELLATION_PUB_KEY_SIZE; + size_t remaining = count; + + response.get_public_keys.which_response = + CONSTELLATION_GET_PUBLIC_KEYS_RESPONSE_RESULT_TAG; + while (true) { + // send response as batched list of public keys + size_t batch_size = CY_MIN(batch_limit, remaining); + result->public_keys_count = batch_size; + + memcpy(result->public_keys, + &pubkey_list[count - remaining], + batch_size * CONSTELLATION_PUB_KEY_SIZE); + + constellation_send_result(&response); + remaining -= batch_size; + if (0 == remaining) { + break; + } + + if (!constellation_get_query(query, which_request) || + !check_which_request( + query, CONSTELLATION_GET_PUBLIC_KEYS_REQUEST_FETCH_NEXT_TAG)) { + return false; + } + } + return true; +} + +static bool get_user_consent(const pb_size_t which_request, + const char *wallet_name) { + char msg[100] = ""; + + if (CONSTELLATION_QUERY_GET_PUBLIC_KEYS_TAG == which_request) { + snprintf(msg, + sizeof(msg), + UI_TEXT_ADD_ACCOUNT_PROMPT, + CONSTELLATION_NAME, + wallet_name); + } else { + snprintf(msg, + sizeof(msg), + UI_TEXT_RECEIVE_PROMPT, + CONSTELLATION_NAME, + wallet_name); + } + + return core_scroll_page(NULL, msg, constellation_send_error); +} + +/***************************************************************************** + * GLOBAL VARIABLES + *****************************************************************************/ + +/***************************************************************************** + * STATIC FUNCTIONS + *****************************************************************************/ + +/***************************************************************************** + * GLOBAL FUNCTIONS + *****************************************************************************/ +bool generate_dag_address(char *address, const uint8_t *pubkey) { + // address = 'DAG' + parity + bs58_last36 + // bs58_last36 = last 36 chars of bs58enc(sha256(pkcs_prefixed_pubkey)) + // parity = sum(numeric chars in bs58_last36) % 9 + // pkcs_prefixed_pubkey = pkcs_prefix + uncompressed_pubkey + // see + // https://github.com/StardustCollective/dag4.js/blob/main/packages/dag4-keystore/src/key-store.ts#L230 + + uint8_t pkcs_prefixed_pubkey[PKCS_PREFIXED_PUBKEY_SIZE] = {0}; + uint8_t key_digest[SHA256_DIGEST_SIZE] = {0}; + char bs58_encoded_key[BS58_ENCODED_SIZE] = "\0"; + size_t res_size = BS58_ENCODED_SIZE; + + memcpy(pkcs_prefixed_pubkey, PKCS_PREFIX, PKCS_PREFIX_SIZE); + memcpy(pkcs_prefixed_pubkey + PKCS_PREFIX_SIZE, + pubkey, + CONSTELLATION_PUB_KEY_SIZE); + + sha256_Raw(pkcs_prefixed_pubkey, sizeof(pkcs_prefixed_pubkey), key_digest); + + if (!b58enc(bs58_encoded_key, &res_size, key_digest, SHA256_DIGEST_SIZE) || + res_size < 36) { + constellation_send_error(ERROR_COMMON_ERROR_UNKNOWN_ERROR_TAG, 2); + return false; + } + + const char *bs58_last36 = bs58_encoded_key + res_size - 36 - 1; + + uint64_t sum = 0; + for (size_t i = 0; i < 36; i++) { + if (bs58_last36[i] >= '0' && bs58_last36[i] <= '9') { + sum += bs58_last36[i] - '0'; + } + } + char parity = (sum % 9) + '0'; + + snprintf(address, + CONSTELLATION_ACCOUNT_ADDRESS_SIZE + 1, + "DAG%c%.*s", + parity, + 36, + bs58_last36); + + return true; +} + +void constellation_get_pub_keys(constellation_query_t *query) { + char wallet_name[NAME_SIZE] = ""; + uint8_t seed[64] = {0}; + + const pb_size_t which_request = query->which_request; + const constellation_get_public_keys_intiate_request_t *init_req = NULL; + pb_size_t which_response; + + if (CONSTELLATION_QUERY_GET_PUBLIC_KEYS_TAG == which_request) { + which_response = CONSTELLATION_RESULT_GET_PUBLIC_KEYS_TAG; + init_req = &query->get_public_keys.initiate; + } else { + which_response = CONSTELLATION_RESULT_GET_USER_VERIFIED_PUBLIC_KEY_TAG; + init_req = &query->get_user_verified_public_key.initiate; + } + + const pb_size_t count = init_req->derivation_paths_count; + + uint8_t pubkey_list[sizeof(init_req->derivation_paths) / + sizeof(constellation_get_public_keys_derivation_path_t)] + [CONSTELLATION_PUB_KEY_SIZE] = {0}; + + if (!check_which_request( + query, CONSTELLATION_GET_PUBLIC_KEYS_REQUEST_INITIATE_TAG) || + !validate_request(init_req, which_request) || + !get_wallet_name_by_id(init_req->wallet_id, + (uint8_t *)wallet_name, + constellation_send_error)) { + return; + } + + // Take user consent to export public key for the wallet + if (!get_user_consent(which_request, wallet_name)) { + return; + } + + set_app_flow_status(CONSTELLATION_GET_PUBLIC_KEYS_STATUS_CONFIRM); + + if (!reconstruct_seed( + init_req->wallet_id, &seed[0], constellation_send_error)) { + memzero(seed, sizeof(seed)); + return; + } + + set_app_flow_status(CONSTELLATION_GET_PUBLIC_KEYS_STATUS_SEED_GENERATED); + delay_scr_init(ui_text_processing, DELAY_SHORT); + + bool result = + fill_public_keys(init_req->derivation_paths, seed, pubkey_list, count); + + // Clear seed as soon as it is not needed + memzero(seed, sizeof(seed)); + + if (!result) { + // send unknown error; do not know failure reason + constellation_send_error(ERROR_COMMON_ERROR_UNKNOWN_ERROR_TAG, 1); + return; + } + + // In case the request is to `CONSTELLATION_QUERY_GET_PUBLIC_KEY_TAG` type, + // then wait for user verification of the address + if (CONSTELLATION_QUERY_GET_USER_VERIFIED_PUBLIC_KEY_TAG == which_request) { + char address[CONSTELLATION_ACCOUNT_ADDRESS_SIZE + 1] = "\0"; + if (!generate_dag_address(address, pubkey_list[0])) { + return; + } + + if (!core_scroll_page( + ui_text_receive_on, address, constellation_send_error)) { + return; + } + set_app_flow_status(CONSTELLATION_GET_PUBLIC_KEYS_STATUS_VERIFY); + } + + if (!send_public_keys( + query, pubkey_list, count, which_request, which_response)) { + return; + } + + delay_scr_init(ui_text_check_cysync_app, DELAY_TIME); +} diff --git a/common/proto-options/constellation/get_public_key.options b/common/proto-options/constellation/get_public_key.options index e45057963..579a84b1b 100644 --- a/common/proto-options/constellation/get_public_key.options +++ b/common/proto-options/constellation/get_public_key.options @@ -2,4 +2,4 @@ constellation.GetPublicKeysDerivationPath.path type:FT_STATIC max_count:5 fixed_length:true constellation.GetPublicKeysIntiateRequest.wallet_id type:FT_STATIC max_size:32 fixed_length:true constellation.GetPublicKeysIntiateRequest.derivation_paths type:FT_STATIC max_count:100 fixed_length:true -constellation.GetPublicKeysResultResponse.public_keys type:FT_STATIC max_size:33 max_count:10 fixed_length:true +constellation.GetPublicKeysResultResponse.public_keys type:FT_STATIC max_size:65 max_count:10 fixed_length:true