diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfed55e41..68f2c168b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,22 +52,26 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.sha }} - - name: Install clang-format - run: sudo apt-get install -y clang-format-14 - - name: Check code formatting run: | + # Use pre-installed clang-format (ubuntu-latest ships with it) + CF=$(command -v clang-format-14 || command -v clang-format || true) + if [ -z "$CF" ]; then + echo "::warning::clang-format not found, skipping lint" + exit 0 + fi + echo "Using: $CF ($($CF --version | head -1))" FAILED=0 for f in $(find include/keepkey lib/firmware lib/board lib/transport/src \ -name '*.c' -o -name '*.h' 2>/dev/null | grep -v generated | grep -v '.pb.'); do - if ! clang-format-14 --style=file --dry-run --Werror "$f" 2>/dev/null; then + if ! $CF --style=file --dry-run --Werror "$f" 2>/dev/null; then echo "::warning file=$f::Formatting differs from .clang-format" FAILED=1 fi done if [ "$FAILED" = "1" ]; then echo "" - echo "Run: clang-format -i to fix formatting" + echo "Run: $CF -i to fix formatting" echo "Note: treating as warning until codebase is reformatted" fi # Warn-only until existing code is reformatted diff --git a/deps/device-protocol b/deps/device-protocol index 323802f17..b07589ff3 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit 323802f17dd44165a5100357df771348c8b49672 +Subproject commit b07589ff386265f894736a490fc163d46f9479b7 diff --git a/deps/python-keepkey b/deps/python-keepkey index f1dd2b684..e89587701 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit f1dd2b6847346abe8ea2985b22d688de4911643f +Subproject commit e8958770152a85af4c2a03f9e93b5d98e8dff44b diff --git a/include/keepkey/firmware/fsm.h b/include/keepkey/firmware/fsm.h index 19e911106..12297bdd5 100644 --- a/include/keepkey/firmware/fsm.h +++ b/include/keepkey/firmware/fsm.h @@ -118,6 +118,10 @@ void fsm_msgMayachainGetAddress(const MayachainGetAddress *msg); void fsm_msgMayachainSignTx(const MayachainSignTx *msg); void fsm_msgMayachainMsgAck(const MayachainMsgAck *msg); +void fsm_msgSolanaGetAddress(const SolanaGetAddress *msg); +void fsm_msgSolanaSignTx(const SolanaSignTx *msg); +void fsm_msgSolanaSignMessage(const SolanaSignMessage *msg); + #if DEBUG_LINK // void fsm_msgDebugLinkDecision(DebugLinkDecision *msg); void fsm_msgDebugLinkGetState(DebugLinkGetState *msg); diff --git a/include/keepkey/firmware/solana.h b/include/keepkey/firmware/solana.h new file mode 100644 index 000000000..7cc3e8761 --- /dev/null +++ b/include/keepkey/firmware/solana.h @@ -0,0 +1,44 @@ +/* + * This file is part of the KeepKey project. + * + * Copyright (C) 2024 KeepKey + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#ifndef KEEPKEY_FIRMWARE_SOLANA_H +#define KEEPKEY_FIRMWARE_SOLANA_H + +#include "trezor/crypto/bip32.h" +#include "messages-solana.pb.h" + +#define SOLANA_ADDRESS_SIZE 50 // Base58-encoded Ed25519 public key (typically 44 chars + null) +#define SOLANA_SIGNATURE_SIZE 64 // Ed25519 signature size + +// Convert Ed25519 public key to Solana Base58 address +bool solana_publicKeyToAddress(const uint8_t public_key[32], char *address, + size_t address_size); + +// Sign Solana transaction (returns false on failure) +bool solana_signTx(const HDNode *node, const SolanaSignTx *msg, + SolanaSignedTx *resp); + +// Sign Solana message (off-chain signature) +void solana_signMessage(const HDNode *node, const uint8_t *message, + size_t message_len, uint8_t *signature_out); + +// Display message to user for confirmation +bool solana_confirmMessage(const uint8_t *message, size_t message_len); + +#endif diff --git a/include/keepkey/firmware/solana_tx.h b/include/keepkey/firmware/solana_tx.h new file mode 100644 index 000000000..a5bbe16a8 --- /dev/null +++ b/include/keepkey/firmware/solana_tx.h @@ -0,0 +1,104 @@ +/* + * This file is part of the KeepKey project. + * + * Copyright (C) 2024 KeepKey + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#ifndef KEEPKEY_FIRMWARE_SOLANA_TX_H +#define KEEPKEY_FIRMWARE_SOLANA_TX_H + +#include +#include +#include + +// Known Solana program IDs +#define SOLANA_SYSTEM_PROGRAM_ID "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +#define SOLANA_TOKEN_PROGRAM_ID "\x06\xdd\xf6\xe1\xd7\x65\xa1\x93\xd9\xcb\xe1\x46\xce\xeb\x79\xac\x1c\xb4\x85\xed\x5f\x5b\x37\x91\x3a\x8c\xf5\x85\x7e\xff\x00\xa9" +#define SOLANA_STAKE_PROGRAM_ID "\x06\xa7\xd5\x17\x18\xc7\x74\xc9\x28\x56\x63\x98\x69\x1d\x5e\xb6\x8b\x5e\xb8\xa3\x9b\x4b\x6d\x5c\x73\x55\x5b\x21\x00\x00\x00\x00" + +// Solana instruction types +typedef enum { + SOLANA_INSTRUCTION_UNKNOWN = 0, + SOLANA_INSTRUCTION_SYSTEM_TRANSFER = 1, + SOLANA_INSTRUCTION_SYSTEM_CREATE_ACCOUNT = 2, + SOLANA_INSTRUCTION_TOKEN_TRANSFER = 3, + SOLANA_INSTRUCTION_TOKEN_TRANSFER_CHECKED = 4, + SOLANA_INSTRUCTION_TOKEN_APPROVE = 5, + SOLANA_INSTRUCTION_STAKE_DELEGATE = 6, + SOLANA_INSTRUCTION_STAKE_WITHDRAW = 7 +} SolanaInstructionType; + +// Parsed instruction data +typedef struct { + SolanaInstructionType type; + uint8_t program_id[32]; + uint8_t num_accounts; + uint8_t account_indices[16]; // Max accounts per instruction + const uint8_t *data; + size_t data_len; +} SolanaInstruction; + +// Parsed transaction +typedef struct { + uint8_t num_signatures; + uint8_t num_required_signatures; + uint8_t num_readonly_signed; + uint8_t num_readonly_unsigned; + uint16_t num_accounts; + uint8_t account_keys[16][32]; // 16 accounts (512 bytes) + uint8_t recent_blockhash[32]; + uint8_t num_instructions; + SolanaInstruction instructions[8]; // 8 instructions +} SolanaParsedTransaction; + +// System Transfer instruction data +typedef struct { + uint64_t lamports; +} SolanaSystemTransfer; + +// Token Transfer instruction data +typedef struct { + uint64_t amount; + uint8_t decimals; // For TransferChecked +} SolanaTokenTransfer; + +// Read Solana compact-u16 varint +bool read_compact_u16(const uint8_t **data, size_t *remaining, uint16_t *out); + +// Parse a raw Solana transaction +bool solana_parseTransaction(const uint8_t *raw_tx, size_t tx_size, + SolanaParsedTransaction *parsed); + +// Identify instruction type +SolanaInstructionType solana_identifyInstruction(const uint8_t *program_id, + const uint8_t *data, + size_t data_len); + +// Parse specific instruction types +bool solana_parseSystemTransfer(const uint8_t *data, size_t len, + SolanaSystemTransfer *transfer); + +bool solana_parseTokenTransfer(const uint8_t *data, size_t len, + SolanaTokenTransfer *transfer); + +// Display transaction to user +bool solana_confirmTransaction(const SolanaParsedTransaction *tx, + const uint8_t *signer_pubkey); + +// Format lamports to SOL string (1 SOL = 1,000,000,000 lamports) +void solana_formatLamports(uint64_t lamports, char *out, size_t out_len); + +#endif // KEEPKEY_FIRMWARE_SOLANA_TX_H diff --git a/include/keepkey/transport/interface.h b/include/keepkey/transport/interface.h index ffebf205d..e9d1c616b 100644 --- a/include/keepkey/transport/interface.h +++ b/include/keepkey/transport/interface.h @@ -35,6 +35,7 @@ #include "messages-tendermint.pb.h" #include "messages-thorchain.pb.h" #include "messages-mayachain.pb.h" +#include "messages-solana.pb.h" #include "types.pb.h" #include "trezor_transport.h" diff --git a/include/keepkey/transport/messages-solana.options b/include/keepkey/transport/messages-solana.options new file mode 100644 index 000000000..645d5cfac --- /dev/null +++ b/include/keepkey/transport/messages-solana.options @@ -0,0 +1,17 @@ +SolanaGetAddress.address_n max_count:8 +SolanaGetAddress.coin_name max_size:21 + +SolanaAddress.address max_size:64 + +SolanaSignTx.address_n max_count:8 +SolanaSignTx.coin_name max_size:21 +SolanaSignTx.raw_tx max_size:2048 + +SolanaSignedTx.signature max_size:64 + +SolanaSignMessage.address_n max_count:8 +SolanaSignMessage.coin_name max_size:21 +SolanaSignMessage.message max_size:1024 + +SolanaMessageSignature.public_key max_size:32 +SolanaMessageSignature.signature max_size:64 diff --git a/include/keepkey/transport/messages-ton.options b/include/keepkey/transport/messages-ton.options new file mode 100644 index 000000000..18c064289 --- /dev/null +++ b/include/keepkey/transport/messages-ton.options @@ -0,0 +1,12 @@ +TonGetAddress.address_n max_count:8 +TonGetAddress.coin_name max_size:21 + +TonAddress.address max_size:50 +TonAddress.raw_address max_size:70 + +TonSignTx.address_n max_count:8 +TonSignTx.coin_name max_size:21 +TonSignTx.raw_tx max_size:1024 +TonSignTx.to_address max_size:50 + +TonSignedTx.signature max_size:64 diff --git a/include/keepkey/transport/messages-tron.options b/include/keepkey/transport/messages-tron.options new file mode 100644 index 000000000..80215e8cf --- /dev/null +++ b/include/keepkey/transport/messages-tron.options @@ -0,0 +1,14 @@ +TronGetAddress.address_n max_count:8 +TronGetAddress.coin_name max_size:21 + +TronAddress.address max_size:35 + +TronSignTx.address_n max_count:8 +TronSignTx.coin_name max_size:21 +TronSignTx.raw_data max_size:2048 +TronSignTx.ref_block_bytes max_size:4 +TronSignTx.ref_block_hash max_size:32 +TronSignTx.contract_type max_size:64 +TronSignTx.to_address max_size:35 + +TronSignedTx.signature max_size:65 diff --git a/include/keepkey/transport/messages.options b/include/keepkey/transport/messages.options index 41e0c377c..ad08bd6ba 100644 --- a/include/keepkey/transport/messages.options +++ b/include/keepkey/transport/messages.options @@ -131,3 +131,5 @@ FlashHash.challenge max_size:32 FlashWrite.data max_size:1024 FlashHashResponse.data max_size:32 + +Bip85Mnemonic.mnemonic max_size:241 diff --git a/lib/firmware/CMakeLists.txt b/lib/firmware/CMakeLists.txt index e08ccfc69..eb76b69dd 100644 --- a/lib/firmware/CMakeLists.txt +++ b/lib/firmware/CMakeLists.txt @@ -33,6 +33,9 @@ set(sources ripple_base58.c signing.c signtx_tendermint.c + solana.c + solana_msg.c + solana_tx.c storage.c tendermint.c thorchain.c diff --git a/lib/firmware/fsm.c b/lib/firmware/fsm.c index 0d779f448..158805aec 100644 --- a/lib/firmware/fsm.c +++ b/lib/firmware/fsm.c @@ -56,6 +56,8 @@ #include "keepkey/firmware/signing.h" #include "keepkey/firmware/signtx_tendermint.h" #include "keepkey/firmware/storage.h" +#include "keepkey/firmware/solana.h" +#include "keepkey/firmware/solana_tx.h" #include "keepkey/firmware/tendermint.h" #include "keepkey/firmware/thorchain.h" #include "keepkey/firmware/transaction.h" @@ -84,6 +86,7 @@ #include "messages-ripple.pb.h" #include "messages-thorchain.pb.h" #include "messages-mayachain.pb.h" +#include "messages-solana.pb.h" #include @@ -284,3 +287,4 @@ void fsm_msgClearSession(ClearSession *msg) { #include "fsm_msg_tendermint.h" #include "fsm_msg_thorchain.h" #include "fsm_msg_mayachain.h" +#include "fsm_msg_solana.h" diff --git a/lib/firmware/fsm_msg_solana.h b/lib/firmware/fsm_msg_solana.h new file mode 100644 index 000000000..f163da372 --- /dev/null +++ b/lib/firmware/fsm_msg_solana.h @@ -0,0 +1,227 @@ +/* + * This file is part of the KeepKey project. + * + * Copyright (C) 2024 KeepKey + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#include "keepkey/firmware/solana_tx.h" + +void fsm_msgSolanaGetAddress(const SolanaGetAddress *msg) { + RESP_INIT(SolanaAddress); + + CHECK_INITIALIZED + + CHECK_PIN + + // Validate BIP44 path: m/44'/501'/... + if (msg->address_n_count < 2 || + msg->address_n[0] != (0x80000000 | 44) || + msg->address_n[1] != (0x80000000 | 501)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Invalid Solana derivation path (expected m/44'/501'/...)")); + layoutHome(); + return; + } + + // Derive Ed25519 key for Solana (uses Ed25519 curve, not secp256k1) + HDNode *node = fsm_getDerivedNode("ed25519", msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + + // Get Ed25519 public key + uint8_t public_key[32]; + ed25519_publickey(node->private_key, public_key); + + // Convert to Solana address (Base58 encoding) + char address[SOLANA_ADDRESS_SIZE]; + if (!solana_publicKeyToAddress(public_key, address, sizeof(address))) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("Solana address derivation failed")); + layoutHome(); + return; + } + + // Copy address to response + resp->has_address = true; + strlcpy(resp->address, address, sizeof(resp->address)); + + // Show address on display if requested + if (msg->has_show_display && msg->show_display) { + char node_str[NODE_STRING_LENGTH]; + + // Format BIP32 path as string (no coin table entry needed) + if (!bip32_path_to_string(node_str, sizeof(node_str), msg->address_n, + msg->address_n_count)) { + memset(node_str, 0, sizeof(node_str)); + } + + // Confirm address display with user + if (!confirm_ethereum_address(node_str, address)) { + memzero(public_key, sizeof(public_key)); + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Show address cancelled")); + layoutHome(); + return; + } + } + + // Clean up and send response + memzero(public_key, sizeof(public_key)); + memzero(node, sizeof(*node)); + msg_write(MessageType_MessageType_SolanaAddress, resp); + layoutHome(); +} + +void fsm_msgSolanaSignTx(const SolanaSignTx *msg) { + RESP_INIT(SolanaSignedTx); + + CHECK_INITIALIZED + + CHECK_PIN + + // Validate BIP44 path: m/44'/501'/... + if (msg->address_n_count < 2 || + msg->address_n[0] != (0x80000000 | 44) || + msg->address_n[1] != (0x80000000 | 501)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Invalid Solana derivation path (expected m/44'/501'/...)")); + layoutHome(); + return; + } + + // Validate transaction data + if (!msg->has_raw_tx || msg->raw_tx.size == 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("No transaction data provided")); + layoutHome(); + return; + } + + // Derive Ed25519 key for Solana + HDNode *node = fsm_getDerivedNode("ed25519", msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + + // Get public key for signer identification + uint8_t public_key[32]; + ed25519_publickey(node->private_key, public_key); + + // Parse transaction to display details to user + SolanaParsedTransaction parsed_tx; + if (!solana_parseTransaction(msg->raw_tx.bytes, msg->raw_tx.size, &parsed_tx)) { + memzero(public_key, sizeof(public_key)); + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Failed to parse transaction")); + layoutHome(); + return; + } + + // Confirm transaction with user (shows parsed details) + if (!solana_confirmTransaction(&parsed_tx, public_key)) { + memzero(public_key, sizeof(public_key)); + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); + layoutHome(); + return; + } + + // Sign the transaction + if (!solana_signTx(node, msg, resp)) { + memzero(public_key, sizeof(public_key)); + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_Other, + _("Failed to sign transaction")); + layoutHome(); + return; + } + + // Clean up and send response + memzero(public_key, sizeof(public_key)); + memzero(node, sizeof(*node)); + msg_write(MessageType_MessageType_SolanaSignedTx, resp); + layoutHome(); +} + +void fsm_msgSolanaSignMessage(const SolanaSignMessage *msg) { + RESP_INIT(SolanaMessageSignature); + + CHECK_INITIALIZED + + CHECK_PIN + + // Validate BIP44 path: m/44'/501'/... + if (msg->address_n_count < 2 || + msg->address_n[0] != (0x80000000 | 44) || + msg->address_n[1] != (0x80000000 | 501)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Invalid Solana derivation path (expected m/44'/501'/...)")); + layoutHome(); + return; + } + + // Validate message data + if (!msg->has_message || msg->message.size == 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("No message provided")); + layoutHome(); + return; + } + + // Derive Ed25519 key for Solana + HDNode *node = fsm_getDerivedNode("ed25519", msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + + // Get public key + uint8_t public_key[32]; + ed25519_publickey(node->private_key, public_key); + + // Confirm message signing with user (shows message preview) + // Default to showing confirmation when client doesn't set the field + if (!msg->has_show_display || msg->show_display) { + if (!solana_confirmMessage(msg->message.bytes, msg->message.size)) { + memzero(node, sizeof(*node)); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Message signing cancelled")); + layoutHome(); + return; + } + } + + // Sign the message + uint8_t signature[SOLANA_SIGNATURE_SIZE]; + solana_signMessage(node, msg->message.bytes, msg->message.size, signature); + + // Copy public key and signature to response + resp->has_public_key = true; + resp->public_key.size = 32; + memcpy(resp->public_key.bytes, public_key, 32); + + resp->has_signature = true; + resp->signature.size = SOLANA_SIGNATURE_SIZE; + memcpy(resp->signature.bytes, signature, SOLANA_SIGNATURE_SIZE); + + // Clean up sensitive data + memzero(signature, sizeof(signature)); + memzero(public_key, sizeof(public_key)); + memzero(node, sizeof(*node)); + + msg_write(MessageType_MessageType_SolanaMessageSignature, resp); + layoutHome(); +} diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index e8e374386..3f736777a 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -73,6 +73,10 @@ MSG_IN(MessageType_MessageType_MayachainSignTx, MayachainSignTx, fsm_msgMayachainSignTx) MSG_IN(MessageType_MessageType_MayachainMsgAck, MayachainMsgAck, fsm_msgMayachainMsgAck) + MSG_IN(MessageType_MessageType_SolanaGetAddress, SolanaGetAddress, fsm_msgSolanaGetAddress) + MSG_IN(MessageType_MessageType_SolanaSignTx, SolanaSignTx, fsm_msgSolanaSignTx) + MSG_IN(MessageType_MessageType_SolanaSignMessage, SolanaSignMessage, fsm_msgSolanaSignMessage) + /* Normal Out Messages */ MSG_OUT(MessageType_MessageType_Success, Success, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_Failure, Failure, NO_PROCESS_FUNC) @@ -132,6 +136,10 @@ MSG_OUT(MessageType_MessageType_MayachainMsgRequest, MayachainMsgRequest, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_MayachainSignedTx, MayachainSignedTx, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_SolanaAddress, SolanaAddress, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_SolanaSignedTx, SolanaSignedTx, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_SolanaMessageSignature, SolanaMessageSignature, NO_PROCESS_FUNC) + #if DEBUG_LINK /* Debug Messages */ DEBUG_IN(MessageType_MessageType_DebugLinkDecision, DebugLinkDecision, NO_PROCESS_FUNC) diff --git a/lib/firmware/signing.c b/lib/firmware/signing.c index ccc38c226..b6f3d4460 100644 --- a/lib/firmware/signing.c +++ b/lib/firmware/signing.c @@ -624,7 +624,7 @@ void signing_init(const SignTx *msg, const CoinType *_coin, branch_id = 0x5BA81B19; // Overwinter break; case 4: - branch_id = 0x76B809BB; // Sapling + branch_id = 0xC8E71055; // NU6 break; } } diff --git a/lib/firmware/solana.c b/lib/firmware/solana.c new file mode 100644 index 000000000..d0630b615 --- /dev/null +++ b/lib/firmware/solana.c @@ -0,0 +1,103 @@ +/* + * This file is part of the KeepKey project. + * + * Copyright (C) 2024 KeepKey + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#include "keepkey/firmware/solana.h" +#include "keepkey/firmware/solana_tx.h" +#include "keepkey/firmware/fsm.h" +#include "trezor/crypto/ed25519-donna/ed25519.h" +#include "trezor/crypto/base58.h" +#include "trezor/crypto/memzero.h" + +#include + +/* + * Convert Ed25519 public key to Solana Base58 address + * + * Solana addresses are simply the Base58-encoded Ed25519 public key (32 bytes) + */ +bool solana_publicKeyToAddress(const uint8_t public_key[32], char *address, + size_t address_size) { + if (!public_key || !address || address_size < SOLANA_ADDRESS_SIZE) { + return false; + } + + // Solana uses plain Base58 encoding (NO checksum) for addresses + // The public key is 32 bytes, which encodes to ~43-44 Base58 characters + size_t len = address_size; + if (!b58enc(address, &len, public_key, 32)) { + return false; + } + + return len > 0; +} + +/* + * Sign Solana transaction + * + * Signs the MESSAGE portion of the raw transaction bytes with Ed25519. + * Solana transaction wire format: [sig_count (compact-u16)][sig_count × 64-byte signatures][message] + * Only the message portion is signed — the signature envelope is NOT part of the signed data. + */ +bool solana_signTx(const HDNode *node, const SolanaSignTx *msg, + SolanaSignedTx *resp) { + if (!node || !msg || !resp) { + return false; + } + + // Validate we have transaction data + if (!msg->has_raw_tx || msg->raw_tx.size == 0) { + return false; + } + + // Extract message bytes by skipping the signature envelope. + const uint8_t *data = msg->raw_tx.bytes; + size_t remaining = msg->raw_tx.size; + + // Read num_signatures (compact-u16) + uint16_t num_sigs; + if (!read_compact_u16(&data, &remaining, &num_sigs)) return false; + + // Skip past the dummy signatures (64 bytes each) + size_t sigs_size = (size_t)num_sigs * 64; + if (remaining < sigs_size) return false; + data += sigs_size; + remaining -= sigs_size; + + // 'data' now points to the message, 'remaining' is the message length + + // Get Ed25519 public key + uint8_t public_key[32]; + ed25519_publickey(node->private_key, public_key); + + // Allocate buffer for signature (64 bytes for Ed25519) + uint8_t signature[SOLANA_SIGNATURE_SIZE]; + + // Sign ONLY the message bytes (not the signature envelope) + ed25519_sign(data, remaining, node->private_key, public_key, signature); + + // Copy signature to response + resp->has_signature = true; + resp->signature.size = SOLANA_SIGNATURE_SIZE; + memcpy(resp->signature.bytes, signature, SOLANA_SIGNATURE_SIZE); + + // Clean up sensitive data + memzero(public_key, sizeof(public_key)); + memzero(signature, sizeof(signature)); + return true; +} diff --git a/lib/firmware/solana_msg.c b/lib/firmware/solana_msg.c new file mode 100644 index 000000000..b90eb2cbc --- /dev/null +++ b/lib/firmware/solana_msg.c @@ -0,0 +1,110 @@ +/* + * This file is part of the KeepKey project. + * + * Copyright (C) 2024 KeepKey + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#include "keepkey/firmware/solana.h" +#include "keepkey/firmware/fsm.h" +#include "keepkey/board/confirm_sm.h" +#include "trezor/crypto/ed25519-donna/ed25519.h" +#include "trezor/crypto/memzero.h" + +#include +#include + +/* + * Sign Solana message (off-chain signature) + * + * This is used for authentication, proof-of-ownership, dApp interactions, etc. + * Solana signs raw message bytes directly with Ed25519 (no prefix). + * This matches Phantom, Solflare, and other standard Solana wallets. + */ +void solana_signMessage(const HDNode *node, const uint8_t *message, + size_t message_len, uint8_t *signature_out) { + if (!node || !message || !signature_out) { + return; + } + + // Get Ed25519 public key + uint8_t public_key[32]; + ed25519_publickey(node->private_key, public_key); + + // Sign the raw message with Ed25519 (no prefix - this is Solana standard) + ed25519_sign(message, message_len, node->private_key, public_key, + signature_out); + + // Clean up sensitive data + memzero(public_key, sizeof(public_key)); +} + +/* + * Display message to user for confirmation + * Shows first N chars of message in readable format + */ +bool solana_confirmMessage(const uint8_t *message, size_t message_len) { + if (!message || message_len == 0) { + return false; + } + + // Prepare message preview (first 64 chars or less) + char preview[65]; + size_t preview_len = message_len < 64 ? message_len : 64; + + // Check if message is printable ASCII + bool is_printable = true; + for (size_t i = 0; i < preview_len; i++) { + if (message[i] < 32 || message[i] > 126) { + is_printable = false; + break; + } + } + + if (is_printable) { + // Show text preview + memcpy(preview, message, preview_len); + preview[preview_len] = '\0'; + + if (message_len > 64) { + // Truncate with ... + preview[61] = '.'; + preview[62] = '.'; + preview[63] = '.'; + preview[64] = '\0'; + } + + return confirm(ButtonRequestType_ButtonRequest_Other, + "Sign Message", + "Sign message:\n%s\n\n(%u bytes total)", + preview, (unsigned int)message_len); + } else { + // Show hex preview for binary data + char hex_preview[17]; + size_t hex_len = message_len < 8 ? message_len : 8; + + for (size_t i = 0; i < hex_len; i++) { + snprintf(hex_preview + (i * 2), 3, "%02x", message[i]); + } + hex_preview[hex_len * 2] = '\0'; + + return confirm(ButtonRequestType_ButtonRequest_Other, + "Sign Message", + "Sign binary message:\n0x%s%s\n\n(%u bytes)", + hex_preview, + message_len > 8 ? "..." : "", + (unsigned int)message_len); + } +} diff --git a/lib/firmware/solana_tx.c b/lib/firmware/solana_tx.c new file mode 100644 index 000000000..dbaed9815 --- /dev/null +++ b/lib/firmware/solana_tx.c @@ -0,0 +1,350 @@ +/* + * This file is part of the KeepKey project. + * + * Copyright (C) 2024 KeepKey + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#include "keepkey/firmware/solana_tx.h" +#include "keepkey/firmware/solana.h" +#include "keepkey/board/confirm_sm.h" +#include "keepkey/board/layout.h" +#include "trezor/crypto/memzero.h" + +#include +#include + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +// Compact-u16 encoding used by Solana (little-endian varint, bit 7 = continuation) +bool read_compact_u16(const uint8_t **data, size_t *remaining, uint16_t *out) { + if (*remaining < 1) return false; + uint16_t val = 0; + int shift = 0; + for (int i = 0; i < 3; i++) { + if (*remaining < 1) return false; + uint8_t b = (*data)[0]; + (*data)++; (*remaining)--; + val |= (uint16_t)(b & 0x7F) << shift; + if ((b & 0x80) == 0) { *out = val; return true; } + shift += 7; + } + return false; // too many continuation bytes +} + +bool solana_parseTransaction(const uint8_t *raw_tx, size_t tx_size, + SolanaParsedTransaction *parsed) { + if (!raw_tx || !parsed || tx_size < 3) { + return false; + } + + memzero(parsed, sizeof(*parsed)); + + const uint8_t *data = raw_tx; + size_t remaining = tx_size; + + // Read number of signatures (compact-u16) + uint16_t num_sigs; + if (!read_compact_u16(&data, &remaining, &num_sigs)) return false; + parsed->num_signatures = num_sigs; + + // Skip signatures (64 bytes each) + size_t sigs_size = num_sigs * 64; + if (remaining < sigs_size) return false; + data += sigs_size; + remaining -= sigs_size; + + // Check for v0 versioned transaction (version byte = 0x80) + bool is_versioned = false; + if (remaining > 0 && data[0] == 0x80) { + is_versioned = true; + data++; // Skip version byte + remaining--; + } + + // Read message header (3 bytes) + if (remaining < 3) return false; + parsed->num_required_signatures = data[0]; + parsed->num_readonly_signed = data[1]; + parsed->num_readonly_unsigned = data[2]; + data += 3; + remaining -= 3; + + // Read account keys (store up to 16, but advance past ALL of them) + uint16_t num_accounts; + if (!read_compact_u16(&data, &remaining, &num_accounts)) return false; + parsed->num_accounts = MIN(num_accounts, 16); + + for (int i = 0; i < num_accounts; i++) { + if (remaining < 32) return false; + if (i < 16) { + memcpy(parsed->account_keys[i], data, 32); + } + data += 32; + remaining -= 32; + } + + // Read recent blockhash + if (remaining < 32) return false; + memcpy(parsed->recent_blockhash, data, 32); + data += 32; + remaining -= 32; + + // Read instructions (store up to 8, but parse ALL to advance data pointer) + uint16_t num_instructions; + if (!read_compact_u16(&data, &remaining, &num_instructions)) return false; + parsed->num_instructions = MIN(num_instructions, 8); + + for (int i = 0; i < num_instructions; i++) { + // Use a stack variable for instructions beyond our storage limit + SolanaInstruction overflow_instr; + SolanaInstruction *instr = (i < 8) ? &parsed->instructions[i] : &overflow_instr; + memzero(instr, sizeof(SolanaInstruction)); + + // Read program ID index + if (remaining < 1) return false; + uint8_t program_idx = data[0]; + data++; + remaining--; + + // Check if program_idx references an account we have stored + if (program_idx < parsed->num_accounts) { + memcpy(instr->program_id, parsed->account_keys[program_idx], 32); + } else if (program_idx < num_accounts || is_versioned) { + // Index beyond stored accounts (but valid in static list) or ALT reference + memzero(instr->program_id, 32); + instr->type = SOLANA_INSTRUCTION_UNKNOWN; + } else { + // Invalid index for legacy transactions + return false; + } + + // Read account indices (store up to 16, but advance past ALL of them) + uint16_t num_acct_indices; + if (!read_compact_u16(&data, &remaining, &num_acct_indices)) return false; + instr->num_accounts = MIN(num_acct_indices, 16); + + if (remaining < num_acct_indices) return false; + for (int j = 0; j < instr->num_accounts; j++) { + instr->account_indices[j] = data[j]; + } + data += num_acct_indices; + remaining -= num_acct_indices; + + // Read instruction data + uint16_t data_len; + if (!read_compact_u16(&data, &remaining, &data_len)) return false; + + if (remaining < data_len) return false; + instr->data = data; + instr->data_len = data_len; + data += data_len; + remaining -= data_len; + + // Identify instruction type + if (instr->type != SOLANA_INSTRUCTION_UNKNOWN) { + instr->type = solana_identifyInstruction(instr->program_id, + instr->data, instr->data_len); + } + } + + // For v0 transactions, skip Address Lookup Tables section + if (is_versioned && remaining > 0) { + // Read number of address table lookups + uint16_t num_alt; + if (!read_compact_u16(&data, &remaining, &num_alt)) return false; + + // Skip each lookup table entry + for (int i = 0; i < num_alt; i++) { + // Skip account key (32 bytes) + if (remaining < 32) return false; + data += 32; + remaining -= 32; + + // Skip writable indexes array + uint16_t num_writable; + if (!read_compact_u16(&data, &remaining, &num_writable)) return false; + if (remaining < num_writable) return false; + data += num_writable; + remaining -= num_writable; + + // Skip readonly indexes array + uint16_t num_readonly; + if (!read_compact_u16(&data, &remaining, &num_readonly)) return false; + if (remaining < num_readonly) return false; + data += num_readonly; + remaining -= num_readonly; + } + } + + return true; +} + +SolanaInstructionType solana_identifyInstruction(const uint8_t *program_id, + const uint8_t *data, + size_t data_len) { + if (!program_id || !data || data_len < 4) { + return SOLANA_INSTRUCTION_UNKNOWN; + } + + // System Program + if (memcmp(program_id, SOLANA_SYSTEM_PROGRAM_ID, 32) == 0) { + uint32_t discriminator = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + + if (discriminator == 2 && data_len == 12) { + return SOLANA_INSTRUCTION_SYSTEM_TRANSFER; + } + if (discriminator == 0) { + return SOLANA_INSTRUCTION_SYSTEM_CREATE_ACCOUNT; + } + } + + // Token Program + if (memcmp(program_id, SOLANA_TOKEN_PROGRAM_ID, 32) == 0) { + if (data_len < 1) return SOLANA_INSTRUCTION_UNKNOWN; + + uint8_t instruction_type = data[0]; + + if (instruction_type == 3) { // Transfer + return SOLANA_INSTRUCTION_TOKEN_TRANSFER; + } + if (instruction_type == 12) { // TransferChecked + return SOLANA_INSTRUCTION_TOKEN_TRANSFER_CHECKED; + } + if (instruction_type == 4) { // Approve + return SOLANA_INSTRUCTION_TOKEN_APPROVE; + } + } + + // Stake Program + if (memcmp(program_id, SOLANA_STAKE_PROGRAM_ID, 32) == 0) { + if (data_len < 4) return SOLANA_INSTRUCTION_UNKNOWN; + + uint32_t instruction_type = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + + if (instruction_type == 2) { + return SOLANA_INSTRUCTION_STAKE_DELEGATE; + } + if (instruction_type == 4) { + return SOLANA_INSTRUCTION_STAKE_WITHDRAW; + } + } + + return SOLANA_INSTRUCTION_UNKNOWN; +} + +bool solana_parseSystemTransfer(const uint8_t *data, size_t len, + SolanaSystemTransfer *transfer) { + if (!data || !transfer || len < 12) { + return false; + } + + // Skip discriminator (4 bytes), read lamports (8 bytes, little-endian) + const uint8_t *lamports_ptr = data + 4; + transfer->lamports = 0; + for (int i = 0; i < 8; i++) { + transfer->lamports |= ((uint64_t)lamports_ptr[i]) << (i * 8); + } + + return true; +} + +bool solana_parseTokenTransfer(const uint8_t *data, size_t len, + SolanaTokenTransfer *transfer) { + if (!data || !transfer || len < 9) { + return false; + } + + // Parse amount (bytes 1-8, little-endian) — common to both Transfer and TransferChecked + transfer->amount = 0; + for (int i = 0; i < 8; i++) { + transfer->amount |= ((uint64_t)data[1 + i]) << (i * 8); + } + + // TransferChecked (instruction_type 12): has extra decimals byte at offset 9 + if (len >= 10 && data[0] == 12) { + transfer->decimals = data[9]; + } else { + transfer->decimals = 9; // Default for SPL tokens + } + return true; +} + +void solana_formatLamports(uint64_t lamports, char *out, size_t out_len) { + if (!out || out_len < 30) return; + + // Convert lamports to SOL (1 SOL = 1,000,000,000 lamports) + uint64_t sol = lamports / 1000000000; + uint64_t remainder = lamports % 1000000000; + + if (remainder == 0) { + snprintf(out, out_len, "%llu SOL", (unsigned long long)sol); + } else { + // Remove trailing zeros + char frac[10]; + snprintf(frac, sizeof(frac), "%09llu", (unsigned long long)remainder); + int len = 9; + while (len > 0 && frac[len - 1] == '0') { + frac[--len] = '\0'; + } + snprintf(out, out_len, "%llu.%s SOL", (unsigned long long)sol, frac); + } +} + +bool solana_confirmTransaction(const SolanaParsedTransaction *tx, + const uint8_t *signer_pubkey) { + (void)signer_pubkey; // Reserved for future use in multi-sig validation + + if (!tx || tx->num_instructions == 0) { + return false; + } + + // For now, handle simple single-instruction transactions + const SolanaInstruction *instr = &tx->instructions[0]; + + if (instr->type == SOLANA_INSTRUCTION_SYSTEM_TRANSFER) { + SolanaSystemTransfer transfer; + if (!solana_parseSystemTransfer(instr->data, instr->data_len, &transfer)) { + return false; + } + + // Get recipient address (account index 1 for System Transfer) + if (instr->num_accounts < 2) return false; + uint8_t to_idx = instr->account_indices[1]; + if (to_idx >= tx->num_accounts) return false; + + char to_address[SOLANA_ADDRESS_SIZE]; + if (!solana_publicKeyToAddress(tx->account_keys[to_idx], + to_address, sizeof(to_address))) { + return false; + } + + char amount_str[32]; + solana_formatLamports(transfer.lamports, amount_str, sizeof(amount_str)); + + return confirm(ButtonRequestType_ButtonRequest_SignTx, + "Solana Transfer", + "Send %s to\n%s?", + amount_str, to_address); + } + + // Unknown or complex transaction - show warning + return confirm(ButtonRequestType_ButtonRequest_SignTx, + "Solana Transaction", + "Sign transaction with %d instruction(s)?", + tx->num_instructions); +} diff --git a/lib/transport/CMakeLists.txt b/lib/transport/CMakeLists.txt index 41b8d5864..d42d3e113 100644 --- a/lib/transport/CMakeLists.txt +++ b/lib/transport/CMakeLists.txt @@ -15,6 +15,9 @@ set(protoc_pb_sources ${DEVICE_PROTOCOL}/messages-tendermint.proto ${DEVICE_PROTOCOL}/messages-thorchain.proto ${DEVICE_PROTOCOL}/messages-mayachain.proto + ${DEVICE_PROTOCOL}/messages-solana.proto + ${DEVICE_PROTOCOL}/messages-tron.proto + ${DEVICE_PROTOCOL}/messages-ton.proto ${DEVICE_PROTOCOL}/messages.proto) set(protoc_pb_options @@ -29,6 +32,9 @@ set(protoc_pb_options ${CMAKE_SOURCE_DIR}/include/keepkey/transport/messages-tendermint.options ${CMAKE_SOURCE_DIR}/include/keepkey/transport/messages-thorchain.options ${CMAKE_SOURCE_DIR}/include/keepkey/transport/messages-mayachain.options + ${CMAKE_SOURCE_DIR}/include/keepkey/transport/messages-solana.options + ${CMAKE_SOURCE_DIR}/include/keepkey/transport/messages-tron.options + ${CMAKE_SOURCE_DIR}/include/keepkey/transport/messages-ton.options ${CMAKE_SOURCE_DIR}/include/keepkey/transport/messages.options) set(protoc_c_sources @@ -43,6 +49,9 @@ set(protoc_c_sources ${CMAKE_BINARY_DIR}/lib/transport/messages-tendermint.pb.c ${CMAKE_BINARY_DIR}/lib/transport/messages-thorchain.pb.c ${CMAKE_BINARY_DIR}/lib/transport/messages-mayachain.pb.c + ${CMAKE_BINARY_DIR}/lib/transport/messages-solana.pb.c + ${CMAKE_BINARY_DIR}/lib/transport/messages-tron.pb.c + ${CMAKE_BINARY_DIR}/lib/transport/messages-ton.pb.c ${CMAKE_BINARY_DIR}/lib/transport/messages.pb.c) set(protoc_c_headers @@ -57,6 +66,9 @@ set(protoc_c_headers ${CMAKE_BINARY_DIR}/include/messages-tendermint.pb.h ${CMAKE_BINARY_DIR}/include/messages-thorchain.pb.h ${CMAKE_BINARY_DIR}/include/messages-mayachain.pb.h + ${CMAKE_BINARY_DIR}/include/messages-solana.pb.h + ${CMAKE_BINARY_DIR}/include/messages-tron.pb.h + ${CMAKE_BINARY_DIR}/include/messages-ton.pb.h ${CMAKE_BINARY_DIR}/include/messages.pb.h) set(protoc_pb_sources_moved @@ -71,6 +83,9 @@ set(protoc_pb_sources_moved ${CMAKE_BINARY_DIR}/lib/transport/messages-tendermint.proto ${CMAKE_BINARY_DIR}/lib/transport/messages-thorchain.proto ${CMAKE_BINARY_DIR}/lib/transport/messages-mayachain.proto + ${CMAKE_BINARY_DIR}/lib/transport/messages-solana.proto + ${CMAKE_BINARY_DIR}/lib/transport/messages-tron.proto + ${CMAKE_BINARY_DIR}/lib/transport/messages-ton.proto ${CMAKE_BINARY_DIR}/lib/transport/messages.proto) add_custom_command( @@ -136,6 +151,18 @@ add_custom_command( ${PROTOC_BINARY} -I. -I/usr/include --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb "--nanopb_out=-f messages-mayachain.options:." messages-mayachain.proto + COMMAND + ${PROTOC_BINARY} -I. -I/usr/include + --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + "--nanopb_out=-f messages-solana.options:." messages-solana.proto + COMMAND + ${PROTOC_BINARY} -I. -I/usr/include + --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + "--nanopb_out=-f messages-tron.options:." messages-tron.proto + COMMAND + ${PROTOC_BINARY} -I. -I/usr/include + --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + "--nanopb_out=-f messages-ton.options:." messages-ton.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb