Skip to content

Commit

Permalink
Merge pull request #60 from coderofstuff/account-support
Browse files Browse the repository at this point in the history
Account support
  • Loading branch information
coderofstuff authored Dec 27, 2023
2 parents 2e5b288 + a12d186 commit ae2bff4
Show file tree
Hide file tree
Showing 63 changed files with 179 additions and 28 deletions.
20 changes: 11 additions & 9 deletions doc/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
| `GET_APP_NAME` | 0x04 | Get ASCII encoded application name |
| `GET_PUBLIC_KEY` | 0x05 | Get public key given BIP32 path |
| `SIGN_TX` | 0x06 | Sign transaction given transaction info, utxos and outputs |
| `SIGN_MESSAGE` | 0x07 | Sign the personal message |

## GET_VERSION

Expand Down Expand Up @@ -55,11 +56,11 @@ Keys for kaspa normally use the derivation path `m/44'/111111'/<account>'/<type>

| CData Part | Description |
| --- | --- |
| `purpose` | Must be `44'` or `80000002c` |
| `coin_type` | Must be `111111'` or `8001b207` |
| `account` | Current wallets all use `80000000` (aka. `0'`) for default account but any value from `00000000` to `11111111` is accepted if passed |
| `type` | Current wallets use either `00000000` for Receive Address or `00000001` for Change Address, but any value from `00000000` to `11111111` is accepted if passed |
| `index` | Any value from `00000000` to `11111111` if passed |
| `purpose` | Must be `44'` or `0x80000002c` |
| `coin_type` | Must be `111111'` or `0x8001b207` |
| `account` | Current wallets all use `0x80000000` (aka. `0'`) for default account but any value from `0x80000000` to `0xFFFFFFFF` is accepted if passed |
| `type` | Current wallets use either `0x00000000` for Receive Address or `0x00000001` for Change Address, but any value from `0x00000000` to `0xFFFFFFFF` is accepted if passed |
| `index` | Any value from `0x00000000` to `0xFFFFFFFF` if passed |

If you want to generate addresses using a root public key,

Expand Down Expand Up @@ -88,7 +89,7 @@ Transactions signed with ECDSA are currently not supported.

| P1 Value | Usage | CData |
| --- | --- | --- |
| 0x00 | Sending transaction metadata | `version (2)` \|\| `output_len (1)` \|\| `input_len (1)` |
| 0x00 | Sending transaction metadata | `version (2)` \|\| `output_len (1)` \|\| `input_len (1)` \|\| `change_address_type (1)` \|\| `change_address_index (4)` \|\| `account (4)` |
| 0x01 | Sending a tx output | `value (8)` \|\| `script_public_key (34/35)` |
| 0x02 | Sending a tx input | `value (8)` \|\| `tx_id (32)` \|\| `address_type (1)` \|\| `address_index (4)` \|\| `outpoint_index (1)` |
| 0x03 | Requesting for next signature | - |
Expand All @@ -102,7 +103,7 @@ Transactions signed with ECDSA are currently not supported.
`P2` value is used only if `P1 in {0x00, 0x01, 0x02}`. If `P1 = 0x03`, `P2` is ignored.

#### Flow
1. Send the first APDU `P1 = 0x00` with the version, output length and input length
1. Send the first APDU `P1 = 0x00` with the version, output length and input length, change address type and index, and account (for UTXOs and change)
2. For each output (up to 2), send `P1 = 0x01` with the output CData
3. For each UTXO input send `P1 = 0x02` with the input CData. When sending the last UTXO input set `P2 = 0x00` to indicate that it is the last APDU. The signatures will later be sent back to you in the same order these inputs come in.
4. [Display] User will be able to view the transaction info and choose to `Approve` or `Reject`.
Expand Down Expand Up @@ -133,12 +134,13 @@ Transactions signed with ECDSA are currently not supported.

| CLA | INS | P1 | P2 | Lc | CData |
| --- | --- | --- | --- | --- | --- |
| 0xE0 | 0x07 | 0x00 | 0x00 | var | `address_type (1)` \|\| `address_index (4)` \|\|<br>`message_len (1 bytes)` \|\| `message (var bytes)` |
| 0xE0 | 0x07 | 0x00 | 0x00 | var | `address_type (1)` \|\| `address_index (4)` \|\|<br>`account (4)` \|\|<br>`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` |
| `address_index` | Any value from `00000000` to `FFFFFFFF` |
| `account` | Any value from `80000000` to `FFFFFFFF` |
| `message_len` | How long the message is. Must be a value from `1` to `128`, inclusive |
| `message` | The message to sign |

Expand Down
4 changes: 4 additions & 0 deletions doc/TRANSACTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ For ECDSA-signed addresses (supported by this app only as a send address), it be
| `n_outputs` | 1 | The number of outputs. Exactly 1 or 2.
| `change_address_type` | 1 | `0` if `RECEIVE` or `1` if `CHANGE`* |
| `change_address_index` | 4 | `0x00000000` to `0xFFFFFFFF`**|
| `account` | 4 | `0x80000000` to `0xFFFFFFFF`, normally should use `0x80000000` (the default account)***|

\* While this will be used for the change, the path may be either `RECEIVE` or `CHANGE`.
This is necessary in case the user wants to send the change back to the same address.
In this case, the `change_address_type` has to be set to `RECEIVE`.

\*\* `change_address_type` and `change_address_index` are ignored if `n_outputs == 1`. If `n_outputs == 2` then the path defined here must resolve to the same `script_public_key` in `outputs[1]`.

\*\*\* `account` is the BIP44 account. A transaction can only come from a single account. Current Kaspa ecosystem only uses `0'` (or `0x80000000`) but support this is in anticipation of wider account-based support.

### Transaction Input

Total bytes: 46
Expand Down
2 changes: 1 addition & 1 deletion fuzzing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Change `~/ledger/app-kaspa` to wherever you actual `app-kaspa` folder is.

```
docker run --rm -it -v ~/ledger/app-kaspa:/app ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest bash
docker run --rm -it -v ~/kaspa/ledger/app-kaspa:/app ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest bash
```

## Compilation
Expand Down
4 changes: 2 additions & 2 deletions src/crypto.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ int crypto_sign_transaction(void) {
transaction_input_t *txin =
&G_context.tx_info.transaction.tx_inputs[G_context.tx_info.signing_input_index];

// 44'/111111'/0'/ address_type / address_index
// 44'/111111'/account'/ address_type / address_index
G_context.bip32_path[0] = 0x8000002C;
G_context.bip32_path[1] = 0x8001b207;
G_context.bip32_path[2] = 0x80000000;
G_context.bip32_path[2] = G_context.tx_info.transaction.account;
G_context.bip32_path[3] = (uint32_t)(txin->address_type);
G_context.bip32_path[4] = txin->address_index;

Expand Down
10 changes: 9 additions & 1 deletion src/handler/sign_msg.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ int handler_sign_msg(buffer_t *cdata) {
return io_send_sw(SW_MESSAGE_ADDRESS_TYPE_FAIL);
}

if (!buffer_read_u32(cdata, &G_context.msg_info.account, BE)) {
return io_send_sw(SW_MESSAGE_ADDRESS_TYPE_FAIL);
}

if (G_context.msg_info.account < 0x80000000) {
return io_send_sw(SW_MESSAGE_ADDRESS_TYPE_FAIL);
}

uint8_t message_len = 0;
if (!buffer_read_u8(cdata, &message_len)) {
return io_send_sw(SW_MESSAGE_LEN_PARSING_FAIL);
Expand Down Expand Up @@ -88,7 +96,7 @@ int handler_sign_msg(buffer_t *cdata) {

G_context.bip32_path[0] = 0x8000002C;
G_context.bip32_path[1] = 0x8001b207;
G_context.bip32_path[2] = 0x80000000;
G_context.bip32_path[2] = G_context.msg_info.account;
G_context.bip32_path[3] = (uint32_t)(G_context.msg_info.address_type);
G_context.bip32_path[4] = G_context.msg_info.address_index;

Expand Down
11 changes: 10 additions & 1 deletion src/transaction/deserialize.c
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,18 @@ parser_status_e transaction_deserialize(buffer_t *buf, transaction_t *tx, uint32
return HEADER_PARSING_ERROR;
}

if (!buffer_read_u32(buf, &tx->account, BE)) {
return HEADER_PARSING_ERROR;
}

// Account must be hardened
if (tx->account < 0x80000000) {
return HEADER_PARSING_ERROR;
}

bip32_path[0] = 0x8000002C;
bip32_path[1] = 0x8001b207;
bip32_path[2] = 0x80000000;
bip32_path[2] = tx->account;
bip32_path[3] = (uint32_t)(change_address_type);
bip32_path[4] = change_address_index;

Expand Down
3 changes: 3 additions & 0 deletions src/transaction/serialize.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ int transaction_serialize(const transaction_t *tx, uint32_t *path, uint8_t *out,
write_u32_be(out, offset, path[4]);
offset += 4;

write_u32_be(out, offset, path[2]);
offset += 4;

return (int) offset;
}

Expand Down
6 changes: 3 additions & 3 deletions src/transaction/tx_validate.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ bool tx_validate_parsed_transaction(transaction_t* tx) {

// Forcing these values. path[3] and path[4]
// would've been set by transaction_deserialize
G_context.bip32_path[0] = 0x8000002C;
G_context.bip32_path[1] = 0x8001b207;
G_context.bip32_path[2] = 0x80000000;
G_context.bip32_path[0] = 0x8000002C; // 44'
G_context.bip32_path[1] = 0x8001b207; // 111111'
G_context.bip32_path[2] = tx->account; // the account

G_context.bip32_path_len = 5;

Expand Down
1 change: 1 addition & 0 deletions src/transaction/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ typedef struct {
// For signature purposes:
// Based on: https://kaspa-mdbook.aspectron.com/transactions/constraints/size.html
uint16_t version;
uint32_t account; // The BIP44 account used for inputs in this transaction
size_t tx_input_len; // check
size_t tx_output_len;

Expand Down
1 change: 1 addition & 0 deletions src/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ typedef struct {
uint8_t message[128]; /// message bytes
uint8_t message_hash[32]; /// message hash
uint8_t signature[MAX_DER_SIG_LEN]; /// signature of the message
uint32_t account; /// The account this message will be signed with
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;
Expand Down
14 changes: 9 additions & 5 deletions tests/application_client/kaspa_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ def hash_init() -> blake2b:

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
message: str,
address_type: int = 0,
address_index: int = 0,
account: int = 0x80000000,
):
self.message: bytes = bytes(message, 'utf8') # var
self.address_type: int = address_type # 1 byte
self.address_index: int = address_index # 4 bytes
self.account: int = account # 4 bytes

def serialize(self) -> bytes:
return b"".join([
self.address_type.to_bytes(1, byteorder="big"),
self.address_index.to_bytes(4, byteorder="big"),
self.account.to_bytes(4, byteorder="big"),
len(self.message).to_bytes(1, byteorder="big"),
self.message
])
Expand Down
3 changes: 3 additions & 0 deletions tests/application_client/kaspa_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ def __init__(self,
outputs: list[TransactionOutput],
change_address_type: int = 0,
change_address_index: int = 0,
account: int = 0x80000000,
do_check: bool = True) -> None:
self.version: int = version
self.inputs: list[TransactionInput] = inputs
self.outputs: list[TransactionOutput] = outputs
self.change_address_type: int = change_address_type
self.change_address_index: int = change_address_index
self.account: int = account

if do_check:
if not 0 <= self.version <= 1:
Expand All @@ -101,6 +103,7 @@ def serialize_first_chunk(self) -> bytes:
len(self.inputs).to_bytes(1, byteorder="big"),
self.change_address_type.to_bytes(1, byteorder="big"),
self.change_address_index.to_bytes(4, byteorder="big"),
self.account.to_bytes(4, byteorder="big"),
])

def serialize(self) -> bytes:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions tests/test_sign_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,63 @@ def test_sign_tx_simple(firmware, backend, navigator, test_name):
assert transaction.get_sighash(0) == sighash
assert check_signature_validity(public_key, der_sig, sighash)

def test_sign_tx_different_account(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'/1'/0/0"

# 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)

# Create the transaction that will be sent to the device for signing
transaction = Transaction(
version=0,
account=0x80000001,
inputs=[
TransactionInput(
value=1100000,
tx_id="40b022362f1a303518e2b49f86f87a317c87b514ca0f3d08ad2e7cf49d08cc70",
address_type=0,
address_index=0,
index=0,
public_key=public_key[1:33]
)
],
outputs=[
TransactionOutput(
value=1090000,
script_public_key="2011a7215f668e921013eb7aac9b7e64b9ec6e757c1b648e89388c919f676aa88cac"
)
]
)

# 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_tx(transaction=transaction):
# 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, _, sighash = unpack_sign_tx_response(response)
assert transaction.get_sighash(0) == sighash
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
client = KaspaCommandSender(backend)
Expand Down
53 changes: 48 additions & 5 deletions tests/test_sign_personal_message_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,50 @@ def test_sign_message_simple(firmware, backend, navigator, test_name):
address_index = 5
message = "Hello Kaspa!"

message_data = PersonalMessage(address_type, address_index, message)
message_data = PersonalMessage(message, address_type, address_index)

# 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_simple_different_account(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'/1'/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
account = 0x80000001 # This is account 1'
message = "Hello Kaspa!"

message_data = PersonalMessage(message, address_type, address_index, account)

# Send the sign device instruction.
# As it requires on-screen validation, the function is asynchronous.
Expand Down Expand Up @@ -66,7 +109,7 @@ def test_sign_message_kanji(firmware, backend, navigator, test_name):
address_index = 3
message = "こんにちは世界"

message_data = PersonalMessage(address_type, address_index, message)
message_data = PersonalMessage(message, address_type, address_index)

# Send the sign device instruction.
# As it requires on-screen validation, the function is asynchronous.
Expand Down Expand Up @@ -101,9 +144,9 @@ def test_sign_message_too_long(firmware, backend, navigator, test_name):

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 = '''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'''

message_data = PersonalMessage(address_type, address_index, message)
message_data = PersonalMessage(message, address_type, address_index)

last_response = client.send_raw_apdu(InsType.SIGN_MESSAGE, p1=P1.P1_INPUTS, p2=P2.P2_LAST, data=message_data.serialize())

Expand All @@ -117,7 +160,7 @@ def test_sign_message_refused(firmware, backend, navigator, test_name):
address_index = 6
message = "Hello Kaspa!"

message_data = PersonalMessage(address_type, address_index, message)
message_data = PersonalMessage(message, address_type, address_index)

if firmware.device.startswith("nano"):
with client.sign_message(message_data=message_data):
Expand Down
Loading

0 comments on commit ae2bff4

Please sign in to comment.