From 456a0bc764561720b5cf91ac08c185f781a59819 Mon Sep 17 00:00:00 2001 From: kralverde Date: Sun, 20 Jun 2021 22:08:51 -0400 Subject: [PATCH 01/20] Started ravencoin work --- Makefile | 2 +- src/btchip_helpers.c | 8 ++++++++ src/main.c | 5 +++++ tests/prepare_tests.sh | 4 ++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1b086394..41b77d3d 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,7 @@ APPNAME ="Resistance" APP_LOAD_PARAMS += --path $(APP_PATH) else ifeq ($(COIN),ravencoin) # Ravencoin -DEFINES += BIP44_COIN_TYPE=175 BIP44_COIN_TYPE_2=175 COIN_P2PKH_VERSION=60 COIN_P2SH_VERSION=122 COIN_FAMILY=1 COIN_COINID=\"Ravencoin\" COIN_COINID_HEADER=\"RAVENCOIN\" COIN_COLOR_HDR=0x2E4A80 COIN_COLOR_DB=0x74829E COIN_COINID_NAME=\"Ravencoin\" COIN_COINID_SHORT=\"RVN\" COIN_KIND=COIN_KIND_RAVENCOIN +DEFINES += BIP44_COIN_TYPE=175 BIP44_COIN_TYPE_2=175 COIN_P2PKH_VERSION=60 COIN_P2SH_VERSION=122 COIN_FAMILY=1 COIN_COINID=\"Raven\" COIN_COINID_HEADER=\"RAVENCOIN\" COIN_COLOR_HDR=0x2E4A80 COIN_COLOR_DB=0x74829E COIN_COINID_NAME=\"Ravencoin\" COIN_COINID_SHORT=\"RVN\" COIN_KIND=COIN_KIND_RAVENCOIN APPNAME ="Ravencoin" APP_LOAD_PARAMS += --path $(APP_PATH) else diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index a7131b0c..0de7eaaa 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -149,6 +149,14 @@ unsigned char btchip_output_script_is_op_call(unsigned char *buffer, return output_script_is_op_create_or_call(buffer, size, 0xC2); } +unsigned char btchip_output_script_is_ravencoin_asset_tag(unsigned char *buffer, + size_t size) { + return (!btchip_output_script_is_regular(buffer) && + !btchip_output_script_is_p2sh(buffer) && + !btchip_output_script_is_op_return(buffer) && (buffer[0] <= 0xEA) && + (buffer[0] == 0xC0); +} + unsigned char btchip_rng_u8_modulo(unsigned char modulo) { unsigned int rng_max = 256 % modulo; unsigned int rng_limit = 256 - rng_max; diff --git a/src/main.c b/src/main.c index e8ae7f50..0081972f 100644 --- a/src/main.c +++ b/src/main.c @@ -856,6 +856,11 @@ void get_address_from_output_script(unsigned char* script, int script_size, char strcpy(out, "OP_CALL"); return; } + if ((G_coin_config->kind == COIN_KIND_RAVENCOIN) && + btchip_output_script_is_ravencoin_asset_tag(script, script_size)) { + strcpy(out, "ASSET TAG"); + return; + } if (btchip_output_script_is_native_witness(script)) { if (G_coin_config->native_segwit_prefix) { segwit_addr_encode( diff --git a/tests/prepare_tests.sh b/tests/prepare_tests.sh index b7623638..2ad2fc4c 100755 --- a/tests/prepare_tests.sh +++ b/tests/prepare_tests.sh @@ -4,5 +4,5 @@ make clean make -j DEBUG=1 # compile optionally with PRINTF mv bin/ tests/bitcoin-bin make clean -make -j DEBUG=1 COIN=bitcoin_testnet -mv bin/ tests/bitcoin-testnet-bin +make -j DEBUG=1 COIN=ravencoin +mv bin/ tests/ravencoin-bin From 03b4d635ba246fc1675b2d98b2579736caf357f2 Mon Sep 17 00:00:00 2001 From: kralverde Date: Sun, 20 Jun 2021 22:09:38 -0400 Subject: [PATCH 02/20] Started ravencoin work --- src/btchip_helpers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index 0de7eaaa..7346528f 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -154,7 +154,7 @@ unsigned char btchip_output_script_is_ravencoin_asset_tag(unsigned char *buffer, return (!btchip_output_script_is_regular(buffer) && !btchip_output_script_is_p2sh(buffer) && !btchip_output_script_is_op_return(buffer) && (buffer[0] <= 0xEA) && - (buffer[0] == 0xC0); + (buffer[0] == 0xC0)); } unsigned char btchip_rng_u8_modulo(unsigned char modulo) { From c5bd44a12eab7e6d255b3a6033b7d27c89991bc1 Mon Sep 17 00:00:00 2001 From: kralverde Date: Mon, 21 Jun 2021 10:34:50 -0400 Subject: [PATCH 03/20] ravencoin work --- include/btchip_helpers.h | 3 +++ src/btchip_helpers.c | 51 +++++++++++++++++++++++++++++++++----- src/main.c | 53 ++++++++++++++++++++++++++++++++-------- 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/include/btchip_helpers.h b/include/btchip_helpers.h index a99504b7..a6003cac 100644 --- a/include/btchip_helpers.h +++ b/include/btchip_helpers.h @@ -40,6 +40,9 @@ unsigned char btchip_output_script_is_op_create(unsigned char *buffer, unsigned char btchip_output_script_is_op_call(unsigned char *buffer, size_t size); +unsigned char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned char *buffer); +unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer, size_t size, int *ptr); + void btchip_sleep16(unsigned short delay); void btchip_sleep32(unsigned long int delayEach, unsigned long int delayRepeat); diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index 7346528f..f0e368cf 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -149,12 +149,51 @@ unsigned char btchip_output_script_is_op_call(unsigned char *buffer, return output_script_is_op_create_or_call(buffer, size, 0xC2); } -unsigned char btchip_output_script_is_ravencoin_asset_tag(unsigned char *buffer, - size_t size) { - return (!btchip_output_script_is_regular(buffer) && - !btchip_output_script_is_p2sh(buffer) && - !btchip_output_script_is_op_return(buffer) && (buffer[0] <= 0xEA) && - (buffer[0] == 0xC0)); +unsigned char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned char *buffer) { + if (btchip_output_script_is_regular(buffer) || + btchip_output_script_is_p2sh(buffer) || + btchip_output_script_is_op_return(buffer) || + (buffer[1] != 0xC0)) { + return -1; + } + if (buffer[2] == 0x50) { + if (buffer[3] == 0x50) { + return 2; + } + return 1; + } + return 0; +} + +unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer, size_t size, int *ptr) { + unsigned int script_ptr = 0; + unsigned int op = -1; + if (buffer[size - 1] != 0x75) { + return 0; + } + while (script_ptr < size - 5) { + op = buffer[script_ptr++]; + if (op == 0xC0) { + if ((buffer[script_ptr+1] == 0x72) && + (buffer[script_ptr+2] == 0x76) && + (buffer[script_ptr+3] == 0x6E)) { + *ptr = script_ptr + 4; + } else { + *ptr = script_ptr + 5; + } + return 1; + } + else if (op <= 0x4E) { + if (op < 0x4C) { + script_ptr += op; + } + else { + script_ptr += (buffer[script_ptr] + 1); + } + //There shouldn't be anything pushed larger than 256 bytes in an asset transfer script + } + } + return 0; } unsigned char btchip_rng_u8_modulo(unsigned char modulo) { diff --git a/src/main.c b/src/main.c index 0081972f..0804bd0b 100644 --- a/src/main.c +++ b/src/main.c @@ -841,6 +841,10 @@ uint8_t prepare_fees() { #define MAIDSAFE_ASSETID 3 #define USDT_ASSETID 31 +#define RAVENCOIN_NULL_ASSET_TAG 0 +#define RAVENCOIN_NULL_ASSET_VERIFIER 1 +#define RAVENCOIN_NULL_ASSET_RESTRICTED 2 + void get_address_from_output_script(unsigned char* script, int script_size, char* out, int out_size) { if (btchip_output_script_is_op_return(script)) { strcpy(out, "OP_RETURN"); @@ -856,10 +860,18 @@ void get_address_from_output_script(unsigned char* script, int script_size, char strcpy(out, "OP_CALL"); return; } - if ((G_coin_config->kind == COIN_KIND_RAVENCOIN) && - btchip_output_script_is_ravencoin_asset_tag(script, script_size)) { - strcpy(out, "ASSET TAG"); - return; + if ((G_coin_config->kind == COIN_KIND_RAVENCOIN)) { + switch(btchip_output_script_try_get_ravencoin_asset_tag_type(script)) { + case RAVENCOIN_NULL_ASSET_TAG: + strcpy(out, "ASSET TAG"); + return; + case RAVENCOIN_NULL_ASSET_VERIFIER: + strcpy(out, "ASSET VERIFIER"); + return; + case RAVENCOIN_NULL_ASSET_RESTRICTED: + strcpy(out, "ASSET RESTRICTED"); + return; + } } if (btchip_output_script_is_native_witness(script)) { if (G_coin_config->native_segwit_prefix) { @@ -912,6 +924,7 @@ uint8_t prepare_single_output() { unsigned int offset = 0; unsigned short textSize; char tmp[80] = {0}; + int ravencoin_asset_ptr = -1; btchip_swap_bytes(amount, btchip_context_D.currentOutput + offset, 8); offset += 8; @@ -947,12 +960,32 @@ uint8_t prepare_single_output() { vars.tmp.fullAmount[textSize + headerLength] = '\0'; } else { - os_memmove(vars.tmp.fullAmount, G_coin_config->name_short, - strlen(G_coin_config->name_short)); - vars.tmp.fullAmount[strlen(G_coin_config->name_short)] = ' '; - btchip_context_D.tmp = - (unsigned char *)(vars.tmp.fullAmount + - strlen(G_coin_config->name_short) + 1); + if ((G_coin_config->kind == COIN_KIND_RAVENCOIN) && + (btchip_output_script_get_ravencoin_asset_ptr(btchip_context_D.currentOutput + offset, \ + sizeof(btchip_context_D.currentOutput) - offset, \ + &ravencoin_asset_ptr))) { + unsigned char type; + unsigned char asset_len; + type = (btchip_context_D.currentOutput + offset)[ravencoin_asset_ptr++]; + asset_len = (btchip_context_D.currentOutput + offset)[ravencoin_asset_ptr++]; + btchip_swap_bytes(vars.tmp.fullAmount, btchip_context_D.currentOutput + offset + ravencoin_asset_ptr, asset_len); + ravencoin_asset_ptr += asset_len; + if (type == 0x6F) { + // Ownership amounts do not have an associated amount; give it 100,000,000 virtual sats + btchip_swap_bytes(amount, {0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00 0x00 0x00}, 8); + } + else { + btchip_swap_bytes(amount, btchip_context_D.currentOutput + offset + ravencoin_asset_ptr, 8); + } + } + else { + os_memmove(vars.tmp.fullAmount, G_coin_config->name_short, + strlen(G_coin_config->name_short)); + vars.tmp.fullAmount[strlen(G_coin_config->name_short)] = ' '; + btchip_context_D.tmp = + (unsigned char *)(vars.tmp.fullAmount + + strlen(G_coin_config->name_short) + 1); + } textSize = btchip_convert_hex_amount_to_displayable(amount); vars.tmp.fullAmount[textSize + strlen(G_coin_config->name_short) + 1] = '\0'; From e49833e0e344328a6c164741575cfbee5a3fea49 Mon Sep 17 00:00:00 2001 From: kralverde Date: Mon, 21 Jun 2021 15:06:53 -0400 Subject: [PATCH 04/20] ravencoin work --- src/main.c | 3 +- tests/bitcoin_client/bitcoin_cmd.py | 6 +- tests/clean_tests.sh | 2 +- tests/conftest.py | 6 +- tests/data/many-to-many/p2pkh/tx.json | 4 +- tests/data/one-to-one/p2pkh/tx.json | 12 +- tests/ravencoin-bin/app.apdu | 24 ++++ tests/ravencoin-bin/app.elf | Bin 0 -> 205012 bytes tests/ravencoin-bin/app.hex | 183 ++++++++++++++++++++++++++ tests/ravencoin-bin/app.sha256 | 1 + tests/test_get_coin_version.py | 2 +- tests/test_get_firmware_version.py | 2 +- tests/test_get_pubkey.py | 42 ------ tests/test_get_random.py | 15 --- tests/test_sign.py | 56 ++++---- tests/test_verify.py | 22 ++++ 16 files changed, 277 insertions(+), 103 deletions(-) create mode 100644 tests/ravencoin-bin/app.apdu create mode 100755 tests/ravencoin-bin/app.elf create mode 100644 tests/ravencoin-bin/app.hex create mode 100644 tests/ravencoin-bin/app.sha256 delete mode 100644 tests/test_get_pubkey.py delete mode 100644 tests/test_get_random.py create mode 100644 tests/test_verify.py diff --git a/src/main.c b/src/main.c index 0804bd0b..445e4504 100644 --- a/src/main.c +++ b/src/main.c @@ -966,13 +966,14 @@ uint8_t prepare_single_output() { &ravencoin_asset_ptr))) { unsigned char type; unsigned char asset_len; + unsigned char one_in_sats[8] = {0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00, 0x00, 0x00}; type = (btchip_context_D.currentOutput + offset)[ravencoin_asset_ptr++]; asset_len = (btchip_context_D.currentOutput + offset)[ravencoin_asset_ptr++]; btchip_swap_bytes(vars.tmp.fullAmount, btchip_context_D.currentOutput + offset + ravencoin_asset_ptr, asset_len); ravencoin_asset_ptr += asset_len; if (type == 0x6F) { // Ownership amounts do not have an associated amount; give it 100,000,000 virtual sats - btchip_swap_bytes(amount, {0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00 0x00 0x00}, 8); + btchip_swap_bytes(amount, one_in_sats, 8); } else { btchip_swap_bytes(amount, btchip_context_D.currentOutput + offset + ravencoin_asset_ptr, 8); diff --git a/tests/bitcoin_client/bitcoin_cmd.py b/tests/bitcoin_client/bitcoin_cmd.py index ec5818fd..94fa63fa 100644 --- a/tests/bitcoin_client/bitcoin_cmd.py +++ b/tests/bitcoin_client/bitcoin_cmd.py @@ -75,7 +75,7 @@ def sign_new_tx(self, sign_pub_keys: List[bytes] = [] for sign_path in sign_paths: sign_pub_key, _, _ = self.get_public_key( - addr_type=AddrType.BECH32, + addr_type=AddrType.Legacy, bip32_path=sign_path, display=False ) @@ -120,9 +120,9 @@ def sign_new_tx(self, scriptSig=script_pub_key, nSequence=0xfffffffd)) - if amount_available - fees > amount: + if False and amount_available - fees > amount: change_pub_key, _, _ = self.get_public_key( - addr_type=AddrType.BECH32, + addr_type=AddrType.Legacy, bip32_path=change_path, display=False ) diff --git a/tests/clean_tests.sh b/tests/clean_tests.sh index 83b9187b..1660cc3d 100755 --- a/tests/clean_tests.sh +++ b/tests/clean_tests.sh @@ -1,4 +1,4 @@ #!/bin/bash rm -rf bitcoin-bin -rm -rf bitcoin-testnet-bin +rm -rf ravencoin-bin diff --git a/tests/conftest.py b/tests/conftest.py index 01dc5c17..ea8f5932 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,10 +33,10 @@ def device(request, hid): speculos_executable = os.environ.get("SPECULOS", "speculos.py") base_args = [ - speculos_executable, "./bitcoin-testnet-bin/app.elf", + speculos_executable, "./ravencoin-bin/app.elf", "-l", "Bitcoin:./bitcoin-bin/app.elf", - "--sdk", "1.6", - "--display", "headless" + "--sdk", "2.0", + #"--display", "headless" ] # Look for the automation_file attribute in the test function, if present diff --git a/tests/data/many-to-many/p2pkh/tx.json b/tests/data/many-to-many/p2pkh/tx.json index ce38b5b4..d35998a6 100644 --- a/tests/data/many-to-many/p2pkh/tx.json +++ b/tests/data/many-to-many/p2pkh/tx.json @@ -4,8 +4,8 @@ "amount": 500000, "fees": 400, "to": "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm", - "sign_paths": ["m/84'/1'/0'/1/0", "m/84'/1'/0'/0/3"], - "change_path": "m/84'/1'/0'/1/2", + "sign_paths": ["m/44'/175'/0'/1/0", "m/44'/175'/0'/0/3"], + "change_path": "m/44'/175'/0'/1/2", "lock_time": 1901785, "utxos": [ { diff --git a/tests/data/one-to-one/p2pkh/tx.json b/tests/data/one-to-one/p2pkh/tx.json index cce076fe..c57043da 100644 --- a/tests/data/one-to-one/p2pkh/tx.json +++ b/tests/data/one-to-one/p2pkh/tx.json @@ -1,10 +1,10 @@ { - "txid": "f26b62046101b7cd369eafb3aed5bef343ff3849b98b3cf42dea9cdc78b4c2f4", - "raw": "02000000015122c2cde6823e55754175b92c9c57a0a8e1ac83c38e1787fd3a1ff3348e9513010000006b483045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f4012102ee8608207e21028426f69e76447d7e3d5e077049f5e683c3136c2314762a4718fdffffff0178410f00000000001976a91413d7d58166946c3ec022934066d8c0d111d1bb4188ac1a041d00", - "amount": 999800, - "fees": 200, + "txid": "f6fb120c5a84876a3ac89b07a0b5768d0942d9f600683886a90d3f305aee4926", + "raw": "020000000233fb9b36f06c2c8eda080f9f3e60751fe048698f30ed68210def89dac24a2958020000006a47304402201518af40074bc83161f23e2aabb869f725986973b508da45729366a70ba724e102204c6be8fbd833eb2e381fb8c92a00a5f98c428553575134e5004ca55c20bc2af70121026e71d6747bdf84db5956b10888505f735e2a7b0abc65c012d5f27dac9361e12afeffffff158b9be19a0f5befdc0e91c9c0e7e71d01b19dd757b60c1668e3b3b3534fdde0000000006a47304402204142a1d896bc79715887a7a0753f94bbcbc81f4df51df66432583c91f252e8380220665f0f0a4949bdc7cc6781098e1064d1a02814013ae43b0560b182282b896042012102b1e17177f4e7f130a844f33ada5bd69b7785e3ec925caf3296a77ee41da47b9cfeffffff03a0525d00000000001976a9142f5ff57ca3dc4cae11312aac78d5b1fc2b96017588ac00000000000000003176a91469fa33a001f6854f463f7bb429d7774227fc826788acc01572766e740852564e544553543100e1f505000000007500000000000000003176a914eaf8a8c494018a84291721a6638ef57ac6d83eae88acc01572766e740852564e544553543100222048020000007563cf0b00", + "amount": 0, + "fees": 456, "to": "mhKsh7EzJo1gSU1vrpyejS1qsJAuKyaWWg", - "sign_paths": ["m/84'/1'/0'/0/0"], + "sign_paths": ["m/175'/1'/0'/0/0"], "change_path": null, "lock_time": 1901594, "utxos": [ @@ -12,7 +12,7 @@ "txid": "13958e34f31f3afd87178ec383ace1a8a0579c2cb9754175553e82e6cdc22251", "raw": "02000000000101ec230e53095256052a2428270eec0498944b10f6f1c578f431c23d0098b4ae5a0100000017160014281539820e2de973ae41ba6004b431c921c4d86dfeffffff02727275000000000017a914c8b906af298c70e603a28c3efc2fae19e6ab280f8740420f00000000001976a914cbae5b50cf939e6f531b8a6b7abd788fe14b029788ac02473044022037ecb4248361aafd4f8c11e705f0fa7a5fbdcd595172fcd5643f3b11beff5d400220020c6d326f6c37d63cecadaf4eb335faedf7c44e05f5ef1d2b68140b023bd13d012103dac82fc0acfcfc36348d4a48a46f01cea77f2b9ece3f8c3b4c99d0b0b2f995d284f21c00", "output_indexes": [1], - "output_amounts": [999800] + "output_amounts": [0] } ] diff --git a/tests/ravencoin-bin/app.apdu b/tests/ravencoin-bin/app.apdu new file mode 100644 index 00000000..d267b6de --- /dev/null +++ b/tests/ravencoin-bin/app.apdu @@ -0,0 +1,24 @@ +e00000000b0c09526176656e636f696e +e0000000150b00000b40000000000000005000000a5000000001 +e0000000050500000000 +e0000000d3060000f0b5a5b0064619ad284600f09ffba88502ac000450d10196273419a800f0b2fa239002ae002548223046294600f070f80127377229480390294802903046093028497944062200f065fb1736264979440a22304600f05efbe5704e20a07056206070522020701b2060760f951f48784400f096f800f01efa0d2802d3ff2000f041fa189502a81790194878441490169738021590019c002c0bd060681690e068189014a800f014fa1598206000f01efa02e014a800f00cfa00f058fa19a9884202d1239800f05efa19a8808d002802d1 +e0000000d30600d0002025b0f0bd00f00df8c0463c007a00af00af00f50900006a0a0000cf090000c80900000446724604487844214600f04ff800f033fa214600f02cfbc809000010b513460446cab2194600f0fffa204610bdb0b582b001ace0700120a07000256570662020700421204600f0f1f9032120462a4600f004fa02b0b0bdf0b581b00d4604469c201149085c03281cd100f0ebf9002804d068460321002200f0f0f96e46b57066203070280a707003273046394600f0cdf9a9b2204600f0c9f900223046394600f0dcf901b0f0bd00020020 +e0000000d30601a083b0f0b58cb011ac0ec4002800d166e1044611a806902078002800d15fe10025002805d0252803d0601940786d1cf7e720462946fff7baff605d252801d06419eae76019441c0026202004900a2005960296334619462278641c00232d2af9d0472a13dc2f2a1edd1346303b0a2b00d3abe0302317465f40374300d0049b0a277743be18303e04930b46e3e7672a04dd722a1ddd732a35d11fe0622a37dc482a6dd1012017e0252a79d02a2a20d02e2a00d08ae021782a2900d086e06178482903d0732901d068297fd1641c012313e0 +e0000000d3060270682a59d1002002901020069b1a1d0692cab21f68012a20dd022a0b46b2d169e02178732969d1022306990a1d069209680591a7e7752a4cd0782a3fd05de0632a50d0642a59d10698011d069105680b950a20002d5ad400215be0002a029b059d05d100217a5c491c002afbd14d1e102847d1002d00d166e73878002b05d0012b11d10595674d7d4402e00595634d7d440f2606400009285cfff70bffa85dfff708ff029b059d7f1c6d1ee5d14be7582a23d10120029001e0702a1ed10698011d069105680b9500200190102022e0601e +e0000000d30603400ee00698011d069105680b95002001900a2017e00698011d069100680b900ba8012171e03878002871d04748784405216ae038462946fff7e9fe71e06d420b9501210191a842039001d901270fe0721e07461646002103983a460b4600f08cf94a1e9141a84202d8721e0029f0d001980028059500d0761e049a0025002809d0d0b2302808d107a82d21017001202946054602e0294600e00121b01e0d280cd807a84019761ed2b20491314600f096f90499761e6d1c002efbd1002903d007a82d2141556d1c002f1cd00298002802d0 +e0000000d30604102348784401e0214878440490039e0598394600f0bdf8314600f040f90498405c07a948553846314600f0b2f86d1cbe420746ecd907a82946fff780feb3e60598471c0f4878440121fff778fe7f1ef8d1ae4200d8a7e6701b00d1a4e6ad1b0a4878440121fff76afe6d1cf8d39be60cb0f0bc01bc03b0004770070000e407000067050000fc0700004b050000ca060000e006000001df002900d170470846fff721fe000080b584b0002002900190034801a9fff7efff04b080bdc0463801006080b584b0002102910190034801a9fff7 +e0000000d30604e0e1ff04b080bdc0460d67006080b582b00020019002486946fff7d4ff02b080bd8d68006080b584b0002102910190034801a9fff7c7ff04b080bdc046be9a006080b584b00191009002486946fff7baff04b080bd8183006080b582b00020019002486946fff7aeff02b080bdbb84006080b586b001ab07c3034801a9fff7a2ff80b206b080bdc046e485006080b582b00020019002486946fff794ff02b080bdb187006080b584b0002102910190034801a9fff787ff04b080bdc046060b0160002243088b4274d303098b425fd3030a +e0000000d30605b08b4244d3030b8b4228d3030c8b420dd3ff22090212ba030c8b4202d31212090265d0030b8b4219d300e0090ac30b8b4201d3cb03c01a5241830b8b4201d38b03c01a5241430b8b4201d34b03c01a5241030b8b4201d30b03c01a5241c30a8b4201d3cb02c01a5241830a8b4201d38b02c01a5241430a8b4201d34b02c01a5241030a8b4201d30b02c01a5241cdd2c3098b4201d3cb01c01a524183098b4201d38b01c01a524143098b4201d34b01c01a524103098b4201d30b01c01a5241c3088b4201d3cb00c01a524183088b4201d3 +e0000000d30606808b00c01a524143088b4201d34b00c01a5241411a00d20146524110467047ffe701b5002000f006f802bdc0460029f7d076e770477047c046f0b5ce46474680b5070099463b0c9c4613041b0c1d000e0061460004140c000c45434b4360436143c0182c0c20188c46834203d980235b029846c444494679437243030c63442d042d0cc918000440198918c0bcb946b046f0bdc04610b500f008f810bd0b0010b511001a0000f00af810bd002310b59a4200d110bdcc5cc4540133f8e703008218934200d1704719700133f9e7f0c04146 +e0000000d30607504a4653465c466d4676467ec02838f0c80020704710307cc890469946a246ab46b54608c82838f0c8081c00d1012018474175746f20417070726f76616c004d616e75616c20417070726f76616c004261636b005075626c6963206b657973206578706f7274004170706c69636174696f6e0069732072656164790053657474696e67730056657273696f6e00312e362e310051756974005369676e006d657373616765004d65737361676520686173680043616e63656c007369676e617475726500526576696577007472616e736163 +e0000000d306082074696f6e00416d6f756e74004164647265737300466565730041636365707400616e642073656e640052656a65637400436f6e6669726d005468652064657269766174696f6e007061746820697320756e757375616c210044657269766174696f6e20706174680052656a65637420696620796f75277265006e6f74207375726500417070726f76652064657269766174696f6e00417070726f766500436f6e6669726d20746f6b656e004578706f7274007075626c6963206b65793f00546865206368616e67652070617468006973 +e0000000d30608f020756e757375616c004368616e6765207061746800546865207369676e2070617468005369676e207061746800556e766572696669656420696e707574730055706461746500204c6564676572204c697665006f722074686972642070617274790077616c6c657420736f66747761726500436f6e74696e75650053656777697420706172736564206f6e63650a00554e4b4e4f574e00524557415244004572726f72203a2046656573206e6f7420636f6e73697374656e74006f6d6e69004f4d4e4920005553445420004d41494420 +e0000000d30609c0004f4d4e492061737365742025642000252e2a4800416464726573732077617320616c726561647920636865636b65640a00416d6f756e74206e6f74206d6174636865640a0041646472657373206e6f74206d6174636865640a0046656573206973206e6f74206d6174636865640a006f75747075742023256400526176656e0048656c6c6f2066726f6d206c697465636f696e0a00426974636f696e00496e736964652061206c696272617279200a000000004f505f52455455524e0000004f505f43524541544500000041535345 +e0000000b3060a905420544147000000415353455420564552494649455200004153534554205245535452494354454400000000526176656e636f696e00657863657074696f6e5b25645d3a204c523d3078253038580a004552524f5200303132333435363738396162636465663031323334353637383941424344454600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +e00000000107 +e00000000908000000000b4067a3 +e0000000050500000b40 +e000000053060000060e07426974636f696e05312e362e310109526176656e636f696e0205312e362e3103290100000000ffffff00ffffffffffff1ffc0ff80fe107c0078003f003f807f81ffc3ffcfffcffffffff040101 +e00000000107 +e000000009080000000000502c4a +e00000000109 diff --git a/tests/ravencoin-bin/app.elf b/tests/ravencoin-bin/app.elf new file mode 100755 index 0000000000000000000000000000000000000000..0e8615ac7f5ea7d91bda301cd0274cfb7c8d208e GIT binary patch literal 205012 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 Date: Mon, 21 Jun 2021 18:50:38 -0400 Subject: [PATCH 05/20] Working assets --- include/btchip_helpers.h | 2 ++ src/btchip_apdu_hash_input_finalize_full.c | 34 ++++++++++++++++----- src/btchip_helpers.c | 31 ++++++++++++++++--- src/main.c | 22 ++++++++----- tests/bitcoin_client/bitcoin_cmd.py | 7 ++++- tests/data/one-to-one/p2pkh/tx.json | 18 +++++------ tests/ravencoin-bin/app.elf | Bin 205012 -> 205020 bytes tests/test_get_coin_version.py | 13 ++++---- tests/test_get_firmware_version.py | 5 +-- tests/test_get_trusted_inputs.py | 20 ++++++------ tests/test_pubkey.py | 12 ++++++++ tests/test_sign.py | 3 ++ tests/test_verify.py | 2 +- 13 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 tests/test_pubkey.py diff --git a/include/btchip_helpers.h b/include/btchip_helpers.h index a6003cac..36b006b0 100644 --- a/include/btchip_helpers.h +++ b/include/btchip_helpers.h @@ -83,6 +83,8 @@ unsigned char bip32_print_path(unsigned char *bip32Path, char* out, unsigned cha #define btchip_set_check_internal_structure_integrity(x) void btchip_swap_bytes(unsigned char *target, unsigned char *source, unsigned char size); +void btchip_swap_bytes_reversed(unsigned char *target, unsigned char *source, + unsigned char size); void btchip_sign_finalhash(void *keyContext, unsigned char *in, unsigned short inlen, diff --git a/src/btchip_apdu_hash_input_finalize_full.c b/src/btchip_apdu_hash_input_finalize_full.c index 5831b25d..0711ba30 100644 --- a/src/btchip_apdu_hash_input_finalize_full.c +++ b/src/btchip_apdu_hash_input_finalize_full.c @@ -42,9 +42,12 @@ void btchip_apdu_hash_input_finalize_full_reset(void) { static bool check_output_displayable() { bool displayable = true; + bool invalid_script = false; + int dummy; unsigned char amount[8], isOpReturn, isP2sh, isNativeSegwit, j, nullAmount = 1; unsigned char isOpCreate, isOpCall; + unsigned char isRavencoinAsset; for (j = 0; j < 8; j++) { if (btchip_context_D.currentOutput[j] != 0) { @@ -52,11 +55,13 @@ static bool check_output_displayable() { break; } } + if (!nullAmount) { btchip_swap_bytes(amount, btchip_context_D.currentOutput, 8); transaction_amount_add_be(btchip_context_D.totalOutputAmount, btchip_context_D.totalOutputAmount, amount); } + isOpReturn = btchip_output_script_is_op_return(btchip_context_D.currentOutput + 8); isP2sh = btchip_output_script_is_p2sh(btchip_context_D.currentOutput + 8); @@ -68,13 +73,28 @@ static bool check_output_displayable() { isOpCall = btchip_output_script_is_op_call(btchip_context_D.currentOutput + 8, sizeof(btchip_context_D.currentOutput) - 8); - if (((G_coin_config->kind == COIN_KIND_QTUM) && - !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && - !isP2sh && !(nullAmount && isOpReturn) && !isOpCreate && !isOpCall) || - (!(G_coin_config->kind == COIN_KIND_QTUM) && - !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && - !isP2sh && !(nullAmount && isOpReturn))) { - PRINTF("Error : Unrecognized output script"); + isRavencoinAsset = + ((-1 != btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8)) || + (btchip_output_script_get_ravencoin_asset_ptr(btchip_context_D.currentOutput + 8, + sizeof(btchip_context_D.currentOutput) - 8, + &dummy))); + if (G_coin_config->kind == COIN_KIND_QTUM) { + invalid_script = + !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && + !isP2sh && !(nullAmount && isOpReturn) && !isOpCreate && !isOpCall; + } + else if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { + invalid_script = + !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && + !isP2sh && !(nullAmount && isOpReturn) && !(nullAmount && isRavencoinAsset); + } + else { + invalid_script = + !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && + !isP2sh && !(nullAmount && isOpReturn); + } + if (invalid_script) { + PRINTF("Error : Unrecognized output script\n"); THROW(EXCEPTION); } if (btchip_context_D.tmpCtx.output.changeInitialized && !isOpReturn) { diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index f0e368cf..3e0e502b 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -18,6 +18,8 @@ #include "btchip_internal.h" #include "btchip_apdu_constants.h" +const unsigned char TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE[] = {0x76, 0xA9, 0x14}; + const unsigned char TRANSACTION_OUTPUT_SCRIPT_PRE[] = { 0x19, 0x76, 0xA9, 0x14}; // script length, OP_DUP, OP_HASH160, address length @@ -59,6 +61,7 @@ const unsigned char ZEN_OUTPUT_SCRIPT_POST[] = { }; // BIP0115 Replay Protection unsigned char btchip_output_script_is_regular(unsigned char *buffer) { + int i; if (G_coin_config->native_segwit_prefix) { if ((os_memcmp(buffer, TRANSACTION_OUTPUT_SCRIPT_P2WPKH_PRE, sizeof(TRANSACTION_OUTPUT_SCRIPT_P2WPKH_PRE)) == 0) || @@ -75,7 +78,17 @@ unsigned char btchip_output_script_is_regular(unsigned char *buffer) { sizeof(ZEN_OUTPUT_SCRIPT_POST)) == 0)) { return 1; } - } else { + } + else if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { + if ((os_memcmp(buffer + 1, TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE, + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE)) == 0) && + (os_memcmp(buffer + 1 + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE) + 20, + TRANSACTION_OUTPUT_SCRIPT_POST, + sizeof(TRANSACTION_OUTPUT_SCRIPT_POST)) == 0)) { + return 1; + } + } + else { if ((os_memcmp(buffer, TRANSACTION_OUTPUT_SCRIPT_PRE, sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE)) == 0) && (os_memcmp(buffer + sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE) + 20, @@ -166,12 +179,14 @@ unsigned char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned cha } unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer, size_t size, int *ptr) { - unsigned int script_ptr = 0; + unsigned int script_ptr = 1; //Skip the first pushdata op unsigned int op = -1; - if (buffer[size - 1] != 0x75) { + unsigned int final_op = buffer[0]; + + if (final_op >= size || buffer[final_op] != 0x75) { return 0; } - while (script_ptr < size - 5) { + while (script_ptr < final_op) { op = buffer[script_ptr++]; if (op == 0xC0) { if ((buffer[script_ptr+1] == 0x72) && @@ -312,6 +327,14 @@ void btchip_swap_bytes(unsigned char *target, unsigned char *source, } } +void btchip_swap_bytes_reversed(unsigned char *target, unsigned char *source, + unsigned char size) { + unsigned char i; + for (i = 0; i < size; i++) { + target[i] = source[i]; + } +} + unsigned short btchip_decode_base58_address(unsigned char *in, unsigned short inlen, unsigned char *out, diff --git a/src/main.c b/src/main.c index 445e4504..3706aa76 100644 --- a/src/main.c +++ b/src/main.c @@ -846,6 +846,7 @@ uint8_t prepare_fees() { #define RAVENCOIN_NULL_ASSET_RESTRICTED 2 void get_address_from_output_script(unsigned char* script, int script_size, char* out, int out_size) { + if (btchip_output_script_is_op_return(script)) { strcpy(out, "OP_RETURN"); return; @@ -925,6 +926,7 @@ uint8_t prepare_single_output() { unsigned short textSize; char tmp[80] = {0}; int ravencoin_asset_ptr = -1; + unsigned char str_len; btchip_swap_bytes(amount, btchip_context_D.currentOutput + offset, 8); offset += 8; @@ -965,12 +967,15 @@ uint8_t prepare_single_output() { sizeof(btchip_context_D.currentOutput) - offset, \ &ravencoin_asset_ptr))) { unsigned char type; - unsigned char asset_len; unsigned char one_in_sats[8] = {0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00, 0x00, 0x00}; type = (btchip_context_D.currentOutput + offset)[ravencoin_asset_ptr++]; - asset_len = (btchip_context_D.currentOutput + offset)[ravencoin_asset_ptr++]; - btchip_swap_bytes(vars.tmp.fullAmount, btchip_context_D.currentOutput + offset + ravencoin_asset_ptr, asset_len); - ravencoin_asset_ptr += asset_len; + str_len = (btchip_context_D.currentOutput + offset)[ravencoin_asset_ptr++]; + btchip_swap_bytes_reversed(vars.tmp.fullAmount, btchip_context_D.currentOutput + offset + ravencoin_asset_ptr, str_len); + ravencoin_asset_ptr += str_len; + vars.tmp.fullAmount[str_len] = ' '; + btchip_context_D.tmp = + (unsigned char *)(vars.tmp.fullAmount + + str_len + 1); if (type == 0x6F) { // Ownership amounts do not have an associated amount; give it 100,000,000 virtual sats btchip_swap_bytes(amount, one_in_sats, 8); @@ -980,15 +985,16 @@ uint8_t prepare_single_output() { } } else { + str_len = strlen(G_coin_config->name_short); os_memmove(vars.tmp.fullAmount, G_coin_config->name_short, - strlen(G_coin_config->name_short)); - vars.tmp.fullAmount[strlen(G_coin_config->name_short)] = ' '; + str_len); + vars.tmp.fullAmount[str_len] = ' '; btchip_context_D.tmp = (unsigned char *)(vars.tmp.fullAmount + - strlen(G_coin_config->name_short) + 1); + str_len + 1); } textSize = btchip_convert_hex_amount_to_displayable(amount); - vars.tmp.fullAmount[textSize + strlen(G_coin_config->name_short) + 1] = + vars.tmp.fullAmount[textSize + str_len + 1] = '\0'; } diff --git a/tests/bitcoin_client/bitcoin_cmd.py b/tests/bitcoin_client/bitcoin_cmd.py index 94fa63fa..0b4897f2 100644 --- a/tests/bitcoin_client/bitcoin_cmd.py +++ b/tests/bitcoin_client/bitcoin_cmd.py @@ -116,6 +116,7 @@ def sign_new_tx(self, hash160(sign_pub_keys[i]) + # hash160(pubkey) b"\x88" + # OP_EQUALVERIFY b"\xac") # OP_CHECKSIG + print(script_pub_key) tx.vin.append(CTxIn(outpoint=COutPoint(h=utxo.sha256, n=output_index), scriptSig=script_pub_key, nSequence=0xfffffffd)) @@ -180,7 +181,11 @@ def sign_new_tx(self, raise Exception(f"Unsupported address: '{address}'") tx.vout.append(CTxOut(nValue=amount, - scriptPubKey=script_pub_key)) + scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvnt\x08SCAMCOIN\x00\xe4\x0bT\x02\x00\x00\x00u')) + + tx.vout.append(CTxOut(nValue=0, + scriptPubKey=bytes.fromhex('c014d4a4a095e02cd6a9b3cf15cf16cc42dc63baf3e006042342544301'))) + for i in range(len(tx.vin)): self.untrusted_hash_tx_input_start(tx=tx, diff --git a/tests/data/one-to-one/p2pkh/tx.json b/tests/data/one-to-one/p2pkh/tx.json index c57043da..788fcbff 100644 --- a/tests/data/one-to-one/p2pkh/tx.json +++ b/tests/data/one-to-one/p2pkh/tx.json @@ -1,19 +1,19 @@ { - "txid": "f6fb120c5a84876a3ac89b07a0b5768d0942d9f600683886a90d3f305aee4926", - "raw": "020000000233fb9b36f06c2c8eda080f9f3e60751fe048698f30ed68210def89dac24a2958020000006a47304402201518af40074bc83161f23e2aabb869f725986973b508da45729366a70ba724e102204c6be8fbd833eb2e381fb8c92a00a5f98c428553575134e5004ca55c20bc2af70121026e71d6747bdf84db5956b10888505f735e2a7b0abc65c012d5f27dac9361e12afeffffff158b9be19a0f5befdc0e91c9c0e7e71d01b19dd757b60c1668e3b3b3534fdde0000000006a47304402204142a1d896bc79715887a7a0753f94bbcbc81f4df51df66432583c91f252e8380220665f0f0a4949bdc7cc6781098e1064d1a02814013ae43b0560b182282b896042012102b1e17177f4e7f130a844f33ada5bd69b7785e3ec925caf3296a77ee41da47b9cfeffffff03a0525d00000000001976a9142f5ff57ca3dc4cae11312aac78d5b1fc2b96017588ac00000000000000003176a91469fa33a001f6854f463f7bb429d7774227fc826788acc01572766e740852564e544553543100e1f505000000007500000000000000003176a914eaf8a8c494018a84291721a6638ef57ac6d83eae88acc01572766e740852564e544553543100222048020000007563cf0b00", - "amount": 0, - "fees": 456, + "txid": "e65ddd4292d7a2f8e52d7c4a8de3ddbda2448e020b6107d93880aa082c6bbcf1", + "raw": "0200000003cdcd1988112215922607a7d25e85315e152f5adfcfdcbe702d4a88a805b92365490000006b483045022100a37db1e91cfe40faa852fbb1e46c35b00f1c42e7fa6b592f590e1ea257483cb60220344d5050c1bf94408bfed67d7e54c91b3067e986e51d3d8f77b834140ea181e4012102ce7896973ed3d6d924312d1aafae39dc6ed8efdd5154bbae070bc28ae0bf6376feffffffd32ec2d64ce2d330ac2bfcbd6bdb9e5f2b454fe51c0423d111a27e719ffc49f2010000006a4730440220061e9dab97e20572fa907052934194d8543cf6b89a664bb9593e2db62a5667ad02207e7291f64e66323dda5305588ec3e8959e3e3980f1465367389f46a01ade21c80121036e5bc605676ab36842a987b74f38ccfa474a09083117d0971a5c4698ba0ce0d2feffffffb477f5b27fef53657020ede50212b5aa7178e1b240e14d531fd03ff97a224e1d2b0000006b483045022100e553618a63bb883efab68850cc13ca1a945c60f4c50d390287db6e071d224c9e02204e5acd4a61132de1e019cb750e18b6a1ef64ca92a60ddb27dc8d7bededf4961e0121028fd17a52d8d4713ec80f460aebe7e8a4dd64ccf04289dc10c6aec932811d6577feffffff028b421200000000001976a9143b5bd63dea6fc743f31f366b678f198581ce8edc88ac00000000000000003176a914adde097ccf77eac3712dbc9209478d20fcc390bd88acc01572766e74085343414d434f494e00e40b540200000075b2971b00", + "amount": 999800, + "fees": 200, "to": "mhKsh7EzJo1gSU1vrpyejS1qsJAuKyaWWg", - "sign_paths": ["m/175'/1'/0'/0/0"], + "sign_paths": ["m/44'/175'/0'/0/0"], "change_path": null, "lock_time": 1901594, "utxos": [ { - "txid": "13958e34f31f3afd87178ec383ace1a8a0579c2cb9754175553e82e6cdc22251", - "raw": "02000000000101ec230e53095256052a2428270eec0498944b10f6f1c578f431c23d0098b4ae5a0100000017160014281539820e2de973ae41ba6004b431c921c4d86dfeffffff02727275000000000017a914c8b906af298c70e603a28c3efc2fae19e6ab280f8740420f00000000001976a914cbae5b50cf939e6f531b8a6b7abd788fe14b029788ac02473044022037ecb4248361aafd4f8c11e705f0fa7a5fbdcd595172fcd5643f3b11beff5d400220020c6d326f6c37d63cecadaf4eb335faedf7c44e05f5ef1d2b68140b023bd13d012103dac82fc0acfcfc36348d4a48a46f01cea77f2b9ece3f8c3b4c99d0b0b2f995d284f21c00", + "txid": "e65ddd4292d7a2f8e52d7c4a8de3ddbda2448e020b6107d93880aa082c6bbcf1", + "raw": "0200000003cdcd1988112215922607a7d25e85315e152f5adfcfdcbe702d4a88a805b92365490000006b483045022100a37db1e91cfe40faa852fbb1e46c35b00f1c42e7fa6b592f590e1ea257483cb60220344d5050c1bf94408bfed67d7e54c91b3067e986e51d3d8f77b834140ea181e4012102ce7896973ed3d6d924312d1aafae39dc6ed8efdd5154bbae070bc28ae0bf6376feffffffd32ec2d64ce2d330ac2bfcbd6bdb9e5f2b454fe51c0423d111a27e719ffc49f2010000006a4730440220061e9dab97e20572fa907052934194d8543cf6b89a664bb9593e2db62a5667ad02207e7291f64e66323dda5305588ec3e8959e3e3980f1465367389f46a01ade21c80121036e5bc605676ab36842a987b74f38ccfa474a09083117d0971a5c4698ba0ce0d2feffffffb477f5b27fef53657020ede50212b5aa7178e1b240e14d531fd03ff97a224e1d2b0000006b483045022100e553618a63bb883efab68850cc13ca1a945c60f4c50d390287db6e071d224c9e02204e5acd4a61132de1e019cb750e18b6a1ef64ca92a60ddb27dc8d7bededf4961e0121028fd17a52d8d4713ec80f460aebe7e8a4dd64ccf04289dc10c6aec932811d6577feffffff028b421200000000001976a9143b5bd63dea6fc743f31f366b678f198581ce8edc88ac00000000000000003176a914adde097ccf77eac3712dbc9209478d20fcc390bd88acc01572766e74085343414d434f494e00e40b540200000075b2971b00", "output_indexes": [1], - "output_amounts": [0] + "output_amounts": [999800] } ] -} +} \ No newline at end of file diff --git a/tests/ravencoin-bin/app.elf b/tests/ravencoin-bin/app.elf index 0e8615ac7f5ea7d91bda301cd0274cfb7c8d208e..0c13ab5e378cec130ea6805ec0ea8bce6845a899 100755 GIT binary patch delta 32679 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 Rz{+i2&#|8Ode?Vq{vTIWA)WvL delta 32555 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 Date: Mon, 21 Jun 2021 19:33:23 -0400 Subject: [PATCH 06/20] More Ravencoin work --- src/btchip_display_variables.h | 4 +-- src/main.c | 4 +++ tests/bitcoin_client/bitcoin_cmd.py | 5 ++- tests/ravencoin-bin/app.apdu | 10 +++--- tests/ravencoin-bin/app.elf | Bin 205020 -> 204996 bytes tests/ravencoin-bin/app.hex | 52 ++++++++++++++-------------- tests/ravencoin-bin/app.sha256 | 2 +- 7 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/btchip_display_variables.h b/src/btchip_display_variables.h index f62263cb..c434c027 100644 --- a/src/btchip_display_variables.h +++ b/src/btchip_display_variables.h @@ -22,9 +22,9 @@ union display_variables { struct { // char addressSummary[40]; // beginning of the output address ... end // of - char fullAddress[65]; // the address - char fullAmount[20]; // full amount + char fullAmount[32+1+17]; // Ravencoin: max asset length + space + max amt w/ decimal + //char fullAmount[20]; // full amount char feesAmount[20]; // fees } tmp; diff --git a/src/main.c b/src/main.c index 3706aa76..edfdfe25 100644 --- a/src/main.c +++ b/src/main.c @@ -996,6 +996,10 @@ uint8_t prepare_single_output() { textSize = btchip_convert_hex_amount_to_displayable(amount); vars.tmp.fullAmount[textSize + str_len + 1] = '\0'; + + PRINTF("%d\n", sizeof(vars.tmp.fullAmount)); + PRINTF("%s\n", vars.tmp.fullAmount); + PRINTF("%d\n", BIP44_COIN_TYPE); } return 1; diff --git a/tests/bitcoin_client/bitcoin_cmd.py b/tests/bitcoin_client/bitcoin_cmd.py index 0b4897f2..4d9859db 100644 --- a/tests/bitcoin_client/bitcoin_cmd.py +++ b/tests/bitcoin_client/bitcoin_cmd.py @@ -181,7 +181,10 @@ def sign_new_tx(self, raise Exception(f"Unsupported address: '{address}'") tx.vout.append(CTxOut(nValue=amount, - scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvnt\x08SCAMCOIN\x00\xe4\x0bT\x02\x00\x00\x00u')) + scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvnt\x10SCAMCOINSCAMCOIN\x00\xa0rN\x18\t\x00\x00u')) + + tx.vout.append(CTxOut(nValue=0, + scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvno\x05TEST!u')) tx.vout.append(CTxOut(nValue=0, scriptPubKey=bytes.fromhex('c014d4a4a095e02cd6a9b3cf15cf16cc42dc63baf3e006042342544301'))) diff --git a/tests/ravencoin-bin/app.apdu b/tests/ravencoin-bin/app.apdu index d267b6de..cded6c18 100644 --- a/tests/ravencoin-bin/app.apdu +++ b/tests/ravencoin-bin/app.apdu @@ -2,21 +2,21 @@ e00000000b0c09526176656e636f696e e0000000150b00000b40000000000000005000000a5000000001 e0000000050500000000 e0000000d3060000f0b5a5b0064619ad284600f09ffba88502ac000450d10196273419a800f0b2fa239002ae002548223046294600f070f80127377229480390294802903046093028497944062200f065fb1736264979440a22304600f05efbe5704e20a07056206070522020701b2060760f951f48784400f096f800f01efa0d2802d3ff2000f041fa189502a81790194878441490169738021590019c002c0bd060681690e068189014a800f014fa1598206000f01efa02e014a800f00cfa00f058fa19a9884202d1239800f05efa19a8808d002802d1 -e0000000d30600d0002025b0f0bd00f00df8c0463c007a00af00af00f50900006a0a0000cf090000c80900000446724604487844214600f04ff800f033fa214600f02cfbc809000010b513460446cab2194600f0fffa204610bdb0b582b001ace0700120a07000256570662020700421204600f0f1f9032120462a4600f004fa02b0b0bdf0b581b00d4604469c201149085c03281cd100f0ebf9002804d068460321002200f0f0f96e46b57066203070280a707003273046394600f0cdf9a9b2204600f0c9f900223046394600f0dcf901b0f0bd00020020 +e0000000d30600d0002025b0f0bd00f00df8c0463c007a00af00af00fd090000720a0000d7090000d00900000446724604487844214600f04ff800f033fa214600f02cfbd009000010b513460446cab2194600f0fffa204610bdb0b582b001ace0700120a07000256570662020700421204600f0f1f9032120462a4600f004fa02b0b0bdf0b581b00d4604469c201149085c03281cd100f0ebf9002804d068460321002200f0f0f96e46b57066203070280a707003273046394600f0cdf9a9b2204600f0c9f900223046394600f0dcf901b0f0bd00020020 e0000000d30601a083b0f0b58cb011ac0ec4002800d166e1044611a806902078002800d15fe10025002805d0252803d0601940786d1cf7e720462946fff7baff605d252801d06419eae76019441c0026202004900a2005960296334619462278641c00232d2af9d0472a13dc2f2a1edd1346303b0a2b00d3abe0302317465f40374300d0049b0a277743be18303e04930b46e3e7672a04dd722a1ddd732a35d11fe0622a37dc482a6dd1012017e0252a79d02a2a20d02e2a00d08ae021782a2900d086e06178482903d0732901d068297fd1641c012313e0 e0000000d3060270682a59d1002002901020069b1a1d0692cab21f68012a20dd022a0b46b2d169e02178732969d1022306990a1d069209680591a7e7752a4cd0782a3fd05de0632a50d0642a59d10698011d069105680b950a20002d5ad400215be0002a029b059d05d100217a5c491c002afbd14d1e102847d1002d00d166e73878002b05d0012b11d10595674d7d4402e00595634d7d440f2606400009285cfff70bffa85dfff708ff029b059d7f1c6d1ee5d14be7582a23d10120029001e0702a1ed10698011d069105680b9500200190102022e0601e e0000000d30603400ee00698011d069105680b95002001900a2017e00698011d069100680b900ba8012171e03878002871d04748784405216ae038462946fff7e9fe71e06d420b9501210191a842039001d901270fe0721e07461646002103983a460b4600f08cf94a1e9141a84202d8721e0029f0d001980028059500d0761e049a0025002809d0d0b2302808d107a82d21017001202946054602e0294600e00121b01e0d280cd807a84019761ed2b20491314600f096f90499761e6d1c002efbd1002903d007a82d2141556d1c002f1cd00298002802d0 -e0000000d30604102348784401e0214878440490039e0598394600f0bdf8314600f040f90498405c07a948553846314600f0b2f86d1cbe420746ecd907a82946fff780feb3e60598471c0f4878440121fff778fe7f1ef8d1ae4200d8a7e6701b00d1a4e6ad1b0a4878440121fff76afe6d1cf8d39be60cb0f0bc01bc03b0004770070000e407000067050000fc0700004b050000ca060000e006000001df002900d170470846fff721fe000080b584b0002002900190034801a9fff7efff04b080bdc0463801006080b584b0002102910190034801a9fff7 +e0000000d30604102348784401e0214878440490039e0598394600f0bdf8314600f040f90498405c07a948553846314600f0b2f86d1cbe420746ecd907a82946fff780feb3e60598471c0f4878440121fff778fe7f1ef8d1ae4200d8a7e6701b00d1a4e6ad1b0a4878440121fff76afe6d1cf8d39be60cb0f0bc01bc03b0004778070000ec07000067050000040800004b050000d2060000e806000001df002900d170470846fff721fe000080b584b0002002900190034801a9fff7efff04b080bdc0463801006080b584b0002102910190034801a9fff7 e0000000d30604e0e1ff04b080bdc0460d67006080b582b00020019002486946fff7d4ff02b080bd8d68006080b584b0002102910190034801a9fff7c7ff04b080bdc046be9a006080b584b00191009002486946fff7baff04b080bd8183006080b582b00020019002486946fff7aeff02b080bdbb84006080b586b001ab07c3034801a9fff7a2ff80b206b080bdc046e485006080b582b00020019002486946fff794ff02b080bdb187006080b584b0002102910190034801a9fff787ff04b080bdc046060b0160002243088b4274d303098b425fd3030a e0000000d30605b08b4244d3030b8b4228d3030c8b420dd3ff22090212ba030c8b4202d31212090265d0030b8b4219d300e0090ac30b8b4201d3cb03c01a5241830b8b4201d38b03c01a5241430b8b4201d34b03c01a5241030b8b4201d30b03c01a5241c30a8b4201d3cb02c01a5241830a8b4201d38b02c01a5241430a8b4201d34b02c01a5241030a8b4201d30b02c01a5241cdd2c3098b4201d3cb01c01a524183098b4201d38b01c01a524143098b4201d34b01c01a524103098b4201d30b01c01a5241c3088b4201d3cb00c01a524183088b4201d3 e0000000d30606808b00c01a524143088b4201d34b00c01a5241411a00d20146524110467047ffe701b5002000f006f802bdc0460029f7d076e770477047c046f0b5ce46474680b5070099463b0c9c4613041b0c1d000e0061460004140c000c45434b4360436143c0182c0c20188c46834203d980235b029846c444494679437243030c63442d042d0cc918000440198918c0bcb946b046f0bdc04610b500f008f810bd0b0010b511001a0000f00af810bd002310b59a4200d110bdcc5cc4540133f8e703008218934200d1704719700133f9e7f0c04146 e0000000d30607504a4653465c466d4676467ec02838f0c80020704710307cc890469946a246ab46b54608c82838f0c8081c00d1012018474175746f20417070726f76616c004d616e75616c20417070726f76616c004261636b005075626c6963206b657973206578706f7274004170706c69636174696f6e0069732072656164790053657474696e67730056657273696f6e00312e362e310051756974005369676e006d657373616765004d65737361676520686173680043616e63656c007369676e617475726500526576696577007472616e736163 e0000000d306082074696f6e00416d6f756e74004164647265737300466565730041636365707400616e642073656e640052656a65637400436f6e6669726d005468652064657269766174696f6e007061746820697320756e757375616c210044657269766174696f6e20706174680052656a65637420696620796f75277265006e6f74207375726500417070726f76652064657269766174696f6e00417070726f766500436f6e6669726d20746f6b656e004578706f7274007075626c6963206b65793f00546865206368616e67652070617468006973 e0000000d30608f020756e757375616c004368616e6765207061746800546865207369676e2070617468005369676e207061746800556e766572696669656420696e707574730055706461746500204c6564676572204c697665006f722074686972642070617274790077616c6c657420736f66747761726500436f6e74696e75650053656777697420706172736564206f6e63650a00554e4b4e4f574e00524557415244004572726f72203a2046656573206e6f7420636f6e73697374656e74006f6d6e69004f4d4e4920005553445420004d41494420 -e0000000d30609c0004f4d4e492061737365742025642000252e2a4800416464726573732077617320616c726561647920636865636b65640a00416d6f756e74206e6f74206d6174636865640a0041646472657373206e6f74206d6174636865640a0046656573206973206e6f74206d6174636865640a006f75747075742023256400526176656e0048656c6c6f2066726f6d206c697465636f696e0a00426974636f696e00496e736964652061206c696272617279200a000000004f505f52455455524e0000004f505f43524541544500000041535345 -e0000000b3060a905420544147000000415353455420564552494649455200004153534554205245535452494354454400000000526176656e636f696e00657863657074696f6e5b25645d3a204c523d3078253038580a004552524f5200303132333435363738396162636465663031323334353637383941424344454600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +e0000000d30609c0004f4d4e49206173736574202564200025640a0025730a00252e2a4800416464726573732077617320616c726561647920636865636b65640a00416d6f756e74206e6f74206d6174636865640a0041646472657373206e6f74206d6174636865640a0046656573206973206e6f74206d6174636865640a006f75747075742023256400526176656e0048656c6c6f2066726f6d206c697465636f696e0a00426974636f696e00496e736964652061206c696272617279200a000000004f505f52455455524e0000004f505f4352454154 +e0000000b3060a9045000000415353455420544147000000415353455420564552494649455200004153534554205245535452494354454400000000526176656e636f696e00657863657074696f6e5b25645d3a204c523d3078253038580a004552524f520030313233343536373839616263646566303132333435363738394142434445460000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 e00000000107 -e00000000908000000000b4067a3 +e00000000908000000000b4030c5 e0000000050500000b40 e000000053060000060e07426974636f696e05312e362e310109526176656e636f696e0205312e362e3103290100000000ffffff00ffffffffffff1ffc0ff80fe107c0078003f003f807f81ffc3ffcfffcffffffff040101 e00000000107 diff --git a/tests/ravencoin-bin/app.elf b/tests/ravencoin-bin/app.elf index 0c13ab5e378cec130ea6805ec0ea8bce6845a899..2eb3deaab0f340b987d5f4c314c98527c0422aaf 100755 GIT binary patch delta 2751 zcmZWp4RBP|6~5=)cW>TXc5MRL5=byVXsD9Q@DuqF!GN($oq2>XHBcyuD3K!A%1rAR9PD67N}bxFgnsvJQ?~K$ zym!v|&N<&X_uTvT$_f9e6aMxrI`^~et=aVZz-^ncr+kcEhV0)uOzG6`28f>FlbVwl zyN;(`FlJheRYG2nj9qj0m=qH7aUaY3sn0!c(<*Z3jiVd6*+cw=Z!$(9V~e$Z5|m-y z0z1Dqhp|2e`=(kHqN*4(7f?JbcR~mXnOCMUhOCS?3t$@NNtn}-;4R>(Zx;if;o*$U z9`(?&c@FSLO!D{=@D&-S~AHk3D4)-FFwqMsnc}1l8%Fn5nZ^LW#>dXcK}GuwjjD2Ii5)G$Snwmf+p4*-n7%8O_%IwUQM& zs6z)`84c=)2PM$2)ImE(>ErA#Xlx&_vLOJNA^W+m@QV5QG@NwlS#2CtIc1dZ??x2m z?#~0#?Jg<9FlU_A6{q0PJ@l-(H{at3b?KmY%>yv=eudZQqW4Voli4&W@FDm?|5r`_ zr5c(+IWbFs{XBxn607yt7bvs}mE( zRO>rOkVOR5e?M9j!7FfG)tCW{1$Tj~t`H{o%nEqq=h`-1HIsHwMJ}2tTwpQgHI(Z$ zgyDp|edYJUaj zwmLnVGGlWErQz7A@HA*|>AB1m8Ys&GWr%MXU>|AlfdtaW#8KAYH~l`mw1S|1c3lTO zC~ELopkK@DcfQxJcMv-ENj$Vb1TwawVV3s*dU~2!{e3MOe@Lw^r#|D$@ow=PN|iY2 z`4#khGUcidswsm8)nGM+XvEE|q1~Je)wh^R=rMO_F-YagUu}> zRq}zP7Li{mY`8;?nB_oQ*xM_4M{`67$-hW)B!96vEOI6PO_C!5y@E-89zam?9u07< zx2@VE`I+Xhl!_>?#=v^N$+6 zJAO#rf0)wfb@j+$DlG45bo9k{?XW1=Cy4uTXO4n1aoF#8oNbc7sb_(^VYjGIH%(OM z4pU0~DKOw7Uc`g~ZL-DTU5#OW$+p@>ifpZIsIRKU-5M8`;V|Ef4iNg4?MO#6RSBw; zd?QAhmT)@zd?dc%Ye1yK{~Y~VMSE^K|AaZBfWH(&v$}i$J^5O2U!%vMIQUrW%?V;W z-++FTa4{EoB&Ci?{7-7n>r_+mZq)0f$A?b%nP`~z_`;A8BXzWq{yu&jjo7}8lJ`X~ z@jhP!(wpEL%IT(3x~}TFDV4UXXg3uWbw}|pa|y-mg?iWr;-MdV1@=erG2QYxybf}| z`*t_Eh=t+}?+%Nt{+oDOY>OXomM=d{Oq}D!%zYT+>#I)Sz>FblXLw4dJwn&S; zo?3ef;{7;!0pcPagRM^uMJ$BW_s7`R z#@K^uS&;vjlHK=${8O9usUw+uDmADNGkF0GsM`?6jUTG~ES^t)Pz$qwu2E44V<=5^ Q0u?D!IO|a30L6(OO8@`> delta 2801 zcmZWp4RBP|6~5=4ckjMkvNUP3knop32quut2KgP@NMUs9CWe5vGSlKfr)(%#r88_2 zL<}ZSl*tIm0GEmeV(AFsr==}@lz3_|CkcV9F~6M%^EST@b21XV2YANmV&EmV!G)Nfz=15D z6UKLeO&(to{sV(A{Q@6582^;P>z}ZR?imF47_i1BulK?@mbu7uRb#iKd67RDc+3Q@ zMRMUT1T8T^Yi*Myl1iL6K@A8>!mb-^6EKgg-Z0YQPzkOMV$#OA2A$@ys%K)D>e3S5Ku~RF;Eg3gxMJOYY4-M zcF0;Or#Q@5p1Kr*rZ>_!tF2J>sef`lg(Z zYk__puirASUvDE!?BjUpJ{cUd9Sw^K_o1iN!s+R3F!;Ch${EyYrKG#XGbuw+mpiwN zUPz_c`rS$zLzneGD=9)Z-0bUE~FA#<@PP4k)?Fgbv97PNXtogyLQpM6uVyr zKGVT{G$TDKgUb|m)J5fJ#bHi!{++Kr+@|;~qn`lxn2qxO-Ey7^b0-q=xYHMvnW|hCRV#k2E^Iv@ zh=jDv7V6CI|gDs#flYJx=j9VcEWPwv*hS!Yu>kGG#Z;MU0;Vd9wSa=dO?;r%ds)XkGFhrxnY^9M>cE%bmg>FE{uN zSBY0=pysP}HYNFP4DmH3e2rdWADqekwhFIJfyR zZ`COM6)G0x-kO-X2Cw?VxD|xMDj)pHviIw@-qwBzyd=GPprCg!ynyl;86^TVlVgZiXu&9bg zc?+6B=)X-fhN41Jx#H(*qE=?oM(qxe{90f+BE$S)e6UoWe&`SdBc>ky5e@`!vCqYi zLov6w+D?~W=MCu0heKKAWtE0);&XcQA(}tuY}_k-G=S3lc09^Q15wDBm2tOnPbVKk zWq)9!;y2@e<`aP!q&K|-{+Mn(NF#ORFlCPDi#H4@BC~mie(W$67X3Po|BCm}k^dnx z{R4^o^Ip~caon!Y0%2YPx!*l) zM4BgCJbhj2HprcLNmLo}lBX_s4dQeH{UgMm@LFPB>TO6wn&9yeEqD!|LCtdqkCB`4 zcH@;Ib^Yt-ON?q*v~Q`EpSI%*i^QXbZaC>{PgdWjhGUFIx~S9 zQQ@f%8&Q=&7ePFhKw}WANH;%jh165fpCNsHwUy=eF15bhoOU_T)0v8Tgx?Ct8}77z z>!aM#)xcrmAz`X@53w%{u`lcUL;NH;?$Hqc-A~8$&$2n6*6WkmJdtkcOAwatdwO^d f7tn`#S`N_X^kN9h$ Date: Mon, 21 Jun 2021 19:48:32 -0400 Subject: [PATCH 07/20] started a new test --- tests/ravencoin-bin/app.apdu | 24 ----- tests/ravencoin-bin/app.elf | Bin 204996 -> 0 bytes tests/ravencoin-bin/app.hex | 183 --------------------------------- tests/ravencoin-bin/app.sha256 | 1 - tests/test_sign.py | 95 ++++++++++++++++- 5 files changed, 94 insertions(+), 209 deletions(-) delete mode 100644 tests/ravencoin-bin/app.apdu delete mode 100755 tests/ravencoin-bin/app.elf delete mode 100644 tests/ravencoin-bin/app.hex delete mode 100644 tests/ravencoin-bin/app.sha256 diff --git a/tests/ravencoin-bin/app.apdu b/tests/ravencoin-bin/app.apdu deleted file mode 100644 index cded6c18..00000000 --- a/tests/ravencoin-bin/app.apdu +++ /dev/null @@ -1,24 +0,0 @@ -e00000000b0c09526176656e636f696e -e0000000150b00000b40000000000000005000000a5000000001 -e0000000050500000000 -e0000000d3060000f0b5a5b0064619ad284600f09ffba88502ac000450d10196273419a800f0b2fa239002ae002548223046294600f070f80127377229480390294802903046093028497944062200f065fb1736264979440a22304600f05efbe5704e20a07056206070522020701b2060760f951f48784400f096f800f01efa0d2802d3ff2000f041fa189502a81790194878441490169738021590019c002c0bd060681690e068189014a800f014fa1598206000f01efa02e014a800f00cfa00f058fa19a9884202d1239800f05efa19a8808d002802d1 -e0000000d30600d0002025b0f0bd00f00df8c0463c007a00af00af00fd090000720a0000d7090000d00900000446724604487844214600f04ff800f033fa214600f02cfbd009000010b513460446cab2194600f0fffa204610bdb0b582b001ace0700120a07000256570662020700421204600f0f1f9032120462a4600f004fa02b0b0bdf0b581b00d4604469c201149085c03281cd100f0ebf9002804d068460321002200f0f0f96e46b57066203070280a707003273046394600f0cdf9a9b2204600f0c9f900223046394600f0dcf901b0f0bd00020020 -e0000000d30601a083b0f0b58cb011ac0ec4002800d166e1044611a806902078002800d15fe10025002805d0252803d0601940786d1cf7e720462946fff7baff605d252801d06419eae76019441c0026202004900a2005960296334619462278641c00232d2af9d0472a13dc2f2a1edd1346303b0a2b00d3abe0302317465f40374300d0049b0a277743be18303e04930b46e3e7672a04dd722a1ddd732a35d11fe0622a37dc482a6dd1012017e0252a79d02a2a20d02e2a00d08ae021782a2900d086e06178482903d0732901d068297fd1641c012313e0 -e0000000d3060270682a59d1002002901020069b1a1d0692cab21f68012a20dd022a0b46b2d169e02178732969d1022306990a1d069209680591a7e7752a4cd0782a3fd05de0632a50d0642a59d10698011d069105680b950a20002d5ad400215be0002a029b059d05d100217a5c491c002afbd14d1e102847d1002d00d166e73878002b05d0012b11d10595674d7d4402e00595634d7d440f2606400009285cfff70bffa85dfff708ff029b059d7f1c6d1ee5d14be7582a23d10120029001e0702a1ed10698011d069105680b9500200190102022e0601e -e0000000d30603400ee00698011d069105680b95002001900a2017e00698011d069100680b900ba8012171e03878002871d04748784405216ae038462946fff7e9fe71e06d420b9501210191a842039001d901270fe0721e07461646002103983a460b4600f08cf94a1e9141a84202d8721e0029f0d001980028059500d0761e049a0025002809d0d0b2302808d107a82d21017001202946054602e0294600e00121b01e0d280cd807a84019761ed2b20491314600f096f90499761e6d1c002efbd1002903d007a82d2141556d1c002f1cd00298002802d0 -e0000000d30604102348784401e0214878440490039e0598394600f0bdf8314600f040f90498405c07a948553846314600f0b2f86d1cbe420746ecd907a82946fff780feb3e60598471c0f4878440121fff778fe7f1ef8d1ae4200d8a7e6701b00d1a4e6ad1b0a4878440121fff76afe6d1cf8d39be60cb0f0bc01bc03b0004778070000ec07000067050000040800004b050000d2060000e806000001df002900d170470846fff721fe000080b584b0002002900190034801a9fff7efff04b080bdc0463801006080b584b0002102910190034801a9fff7 -e0000000d30604e0e1ff04b080bdc0460d67006080b582b00020019002486946fff7d4ff02b080bd8d68006080b584b0002102910190034801a9fff7c7ff04b080bdc046be9a006080b584b00191009002486946fff7baff04b080bd8183006080b582b00020019002486946fff7aeff02b080bdbb84006080b586b001ab07c3034801a9fff7a2ff80b206b080bdc046e485006080b582b00020019002486946fff794ff02b080bdb187006080b584b0002102910190034801a9fff787ff04b080bdc046060b0160002243088b4274d303098b425fd3030a -e0000000d30605b08b4244d3030b8b4228d3030c8b420dd3ff22090212ba030c8b4202d31212090265d0030b8b4219d300e0090ac30b8b4201d3cb03c01a5241830b8b4201d38b03c01a5241430b8b4201d34b03c01a5241030b8b4201d30b03c01a5241c30a8b4201d3cb02c01a5241830a8b4201d38b02c01a5241430a8b4201d34b02c01a5241030a8b4201d30b02c01a5241cdd2c3098b4201d3cb01c01a524183098b4201d38b01c01a524143098b4201d34b01c01a524103098b4201d30b01c01a5241c3088b4201d3cb00c01a524183088b4201d3 -e0000000d30606808b00c01a524143088b4201d34b00c01a5241411a00d20146524110467047ffe701b5002000f006f802bdc0460029f7d076e770477047c046f0b5ce46474680b5070099463b0c9c4613041b0c1d000e0061460004140c000c45434b4360436143c0182c0c20188c46834203d980235b029846c444494679437243030c63442d042d0cc918000440198918c0bcb946b046f0bdc04610b500f008f810bd0b0010b511001a0000f00af810bd002310b59a4200d110bdcc5cc4540133f8e703008218934200d1704719700133f9e7f0c04146 -e0000000d30607504a4653465c466d4676467ec02838f0c80020704710307cc890469946a246ab46b54608c82838f0c8081c00d1012018474175746f20417070726f76616c004d616e75616c20417070726f76616c004261636b005075626c6963206b657973206578706f7274004170706c69636174696f6e0069732072656164790053657474696e67730056657273696f6e00312e362e310051756974005369676e006d657373616765004d65737361676520686173680043616e63656c007369676e617475726500526576696577007472616e736163 -e0000000d306082074696f6e00416d6f756e74004164647265737300466565730041636365707400616e642073656e640052656a65637400436f6e6669726d005468652064657269766174696f6e007061746820697320756e757375616c210044657269766174696f6e20706174680052656a65637420696620796f75277265006e6f74207375726500417070726f76652064657269766174696f6e00417070726f766500436f6e6669726d20746f6b656e004578706f7274007075626c6963206b65793f00546865206368616e67652070617468006973 -e0000000d30608f020756e757375616c004368616e6765207061746800546865207369676e2070617468005369676e207061746800556e766572696669656420696e707574730055706461746500204c6564676572204c697665006f722074686972642070617274790077616c6c657420736f66747761726500436f6e74696e75650053656777697420706172736564206f6e63650a00554e4b4e4f574e00524557415244004572726f72203a2046656573206e6f7420636f6e73697374656e74006f6d6e69004f4d4e4920005553445420004d41494420 -e0000000d30609c0004f4d4e49206173736574202564200025640a0025730a00252e2a4800416464726573732077617320616c726561647920636865636b65640a00416d6f756e74206e6f74206d6174636865640a0041646472657373206e6f74206d6174636865640a0046656573206973206e6f74206d6174636865640a006f75747075742023256400526176656e0048656c6c6f2066726f6d206c697465636f696e0a00426974636f696e00496e736964652061206c696272617279200a000000004f505f52455455524e0000004f505f4352454154 -e0000000b3060a9045000000415353455420544147000000415353455420564552494649455200004153534554205245535452494354454400000000526176656e636f696e00657863657074696f6e5b25645d3a204c523d3078253038580a004552524f520030313233343536373839616263646566303132333435363738394142434445460000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -e00000000107 -e00000000908000000000b4030c5 -e0000000050500000b40 -e000000053060000060e07426974636f696e05312e362e310109526176656e636f696e0205312e362e3103290100000000ffffff00ffffffffffff1ffc0ff80fe107c0078003f003f807f81ffc3ffcfffcffffffff040101 -e00000000107 -e000000009080000000000502c4a -e00000000109 diff --git a/tests/ravencoin-bin/app.elf b/tests/ravencoin-bin/app.elf deleted file mode 100755 index 2eb3deaab0f340b987d5f4c314c98527c0422aaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204996 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�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 diff --git a/tests/ravencoin-bin/app.hex b/tests/ravencoin-bin/app.hex deleted file mode 100644 index 17491e60..00000000 --- a/tests/ravencoin-bin/app.hex +++ /dev/null @@ -1,183 +0,0 @@ -:02000004C0D06A -:10000000F0B5A5B0064619AD284600F09FFBA885BF -:1000100002AC000450D10196273419A800F0B2FABE -:10002000239002AE002548223046294600F070F8A1 -:100030000127377229480390294802903046093039 -:1000400028497944062200F065FB17362649794491 -:100050000A22304600F05EFBE5704E20A07056206C -:100060006070522020701B2060760F951F487844E6 -:1000700000F096F800F01EFA0D2802D3FF2000F0E1 -:1000800041FA189502A817901948784414901697C9 -:1000900038021590019C002C0BD060681690E06827 -:1000A000189014A800F014FA1598206000F01EFAB9 -:1000B00002E014A800F00CFA00F058FA19A98842DE -:1000C00002D1239800F05EFA19A8808D002802D191 -:1000D000002025B0F0BD00F00DF8C0463C007A00CD -:1000E000AF00AF00FD090000720A0000D709000050 -:1000F000D00900000446724604487844214600F0C6 -:100100004FF800F033FA214600F02CFBD009000034 -:1001100010B513460446CAB2194600F0FFFA20464D -:1001200010BDB0B582B001ACE0700120A070002518 -:100130006570662020700421204600F0F1F903214B -:1001400020462A4600F004FA02B0B0BDF0B581B0F6 -:100150000D4604469C201149085C03281CD100F080 -:10016000EBF9002804D068460321002200F0F0F9E2 -:100170006E46B57066203070280A707003273046CE -:10018000394600F0CDF9A9B2204600F0C9F90022A5 -:100190003046394600F0DCF901B0F0BD0002002025 -:1001A00083B0F0B58CB011AC0EC4002800D166E16C -:1001B000044611A806902078002800D15FE10025B0 -:1001C000002805D0252803D0601940786D1CF7E77A -:1001D00020462946FFF7BAFF605D252801D0641943 -:1001E000EAE76019441C0026202004900A200596A6 -:1001F0000296334619462278641C00232D2AF9D032 -:10020000472A13DC2F2A1EDD1346303B0A2B00D36E -:10021000ABE0302317465F40374300D0049B0A27EA -:100220007743BE18303E04930B46E3E7672A04DDAC -:10023000722A1DDD732A35D11FE0622A37DC482A75 -:100240006DD1012017E0252A79D02A2A20D02E2A24 -:1002500000D08AE021782A2900D086E061784829F8 -:1002600003D0732901D068297FD1641C012313E0D6 -:10027000682A59D1002002901020069B1A1D069270 -:10028000CAB21F68012A20DD022A0B46B2D169E0FA -:100290002178732969D1022306990A1D06920968FB -:1002A0000591A7E7752A4CD0782A3FD05DE0632AF4 -:1002B00050D0642A59D10698011D069105680B9506 -:1002C0000A20002D5AD400215BE0002A029B059DE4 -:1002D00005D100217A5C491C002AFBD14D1E102853 -:1002E00047D1002D00D166E73878002B05D0012BCF -:1002F00011D10595674D7D4402E00595634D7D4420 -:100300000F2606400009285CFFF70BFFA85DFFF7EA -:1003100008FF029B059D7F1C6D1EE5D14BE7582A07 -:1003200023D10120029001E0702A1ED10698011D00 -:10033000069105680B9500200190102022E0601EB8 -:100340000EE00698011D069105680B9500200190AE -:100350000A2017E00698011D069100680B900BA873 -:10036000012171E03878002871D047487844052190 -:100370006AE038462946FFF7E9FE71E06D420B95C9 -:1003800001210191A842039001D901270FE0721EBB -:1003900007461646002103983A460B4600F08CF9B2 -:1003A0004A1E9141A84202D8721E0029F0D001983D -:1003B0000028059500D0761E049A0025002809D053 -:1003C000D0B2302808D107A82D210170012029467C -:1003D000054602E0294600E00121B01E0D280CD898 -:1003E00007A84019761ED2B20491314600F096F962 -:1003F0000499761E6D1C002EFBD1002903D007A89E -:100400002D2141556D1C002F1CD00298002802D0D0 -:100410002348784401E0214878440490039E0598DD -:10042000394600F0BDF8314600F040F90498405CD0 -:1004300007A948553846314600F0B2F86D1CBE4257 -:100440000746ECD907A82946FFF780FEB3E60598D2 -:10045000471C0F4878440121FFF778FE7F1EF8D132 -:10046000AE4200D8A7E6701B00D1A4E6AD1B0A4837 -:1004700078440121FFF76AFE6D1CF8D39BE60CB0AF -:10048000F0BC01BC03B0004778070000EC07000097 -:1004900067050000040800004B050000D2060000BC -:1004A000E806000001DF002900D170470846FFF789 -:1004B00021FE000080B584B0002002900190034826 -:1004C00001A9FFF7EFFF04B080BDC046380100600E -:1004D00080B584B0002102910190034801A9FFF783 -:1004E000E1FF04B080BDC0460D67006080B582B0FA -:1004F0000020019002486946FFF7D4FF02B080BD9A -:100500008D68006080B584B000210291019003489D -:1005100001A9FFF7C7FF04B080BDC046BE9A0060C6 -:1005200080B584B00191009002486946FFF7BAFF98 -:1005300004B080BD8183006080B582B0002001904E -:1005400002486946FFF7AEFF02B080BDBB84006081 -:1005500080B586B001AB07C3034801A9FFF7A2FF2E -:1005600080B206B080BDC046E485006080B582B030 -:100570000020019002486946FFF794FF02B080BD59 -:10058000B187006080B584B00021029101900348DA -:1005900001A9FFF787FF04B080BDC046060B0160CC -:1005A000002243088B4274D303098B425FD3030AB2 -:1005B0008B4244D3030B8B4228D3030C8B420DD3C5 -:1005C000FF22090212BA030C8B4202D31212090253 -:1005D00065D0030B8B4219D300E0090AC30B8B4291 -:1005E00001D3CB03C01A5241830B8B4201D38B033F -:1005F000C01A5241430B8B4201D34B03C01A5241E4 -:10060000030B8B4201D30B03C01A5241C30A8B4226 -:1006100001D3CB02C01A5241830A8B4201D38B0211 -:10062000C01A5241430A8B4201D34B02C01A5241B5 -:10063000030A8B4201D30B02C01A5241CDD2C30927 -:100640008B4201D3CB01C01A524183098B4201D3A3 -:100650008B01C01A524143098B4201D34B01C01A8E -:10066000524103098B4201D30B01C01A5241C30806 -:100670008B4201D3CB00C01A524183088B4201D375 -:100680008B00C01A524143088B4201D34B00C01A61 -:100690005241411A00D20146524110467047FFE7CD -:1006A00001B5002000F006F802BDC0460029F7D0D1 -:1006B00076E770477047C046F0B5CE46474680B5EE -:1006C000070099463B0C9C4613041B0C1D000E00B2 -:1006D00061460004140C000C45434B4360436143E6 -:1006E000C0182C0C20188C46834203D980235B024F -:1006F0009846C444494679437243030C63442D042D -:100700002D0CC918000440198918C0BCB946B04660 -:10071000F0BDC04610B500F008F810BD0B0010B5D4 -:1007200011001A0000F00AF810BD002310B59A421B -:1007300000D110BDCC5CC4540133F8E7030082182B -:10074000934200D1704719700133F9E7F0C0414678 -:100750004A4653465C466D4676467EC02838F0C809 -:100760000020704710307CC890469946A246AB46A0 -:10077000B54608C82838F0C8081C00D10120184721 -:100780004175746F20417070726F76616C004D61BD -:100790006E75616C20417070726F76616C004261A1 -:1007A000636B005075626C6963206B6579732065BB -:1007B00078706F7274004170706C69636174696FF6 -:1007C0006E006973207265616479005365747469A1 -:1007D0006E67730056657273696F6E00312E362E28 -:1007E000310051756974005369676E006D657373EC -:1007F000616765004D657373616765206861736843 -:100800000043616E63656C007369676E6174757235 -:100810006500526576696577007472616E73616315 -:1008200074696F6E00416D6F756E7400416464721F -:10083000657373004665657300416363657074009A -:10084000616E642073656E640052656A656374004E -:10085000436F6E6669726D005468652064657269E5 -:10086000766174696F6E00706174682069732075B9 -:100870006E757375616C2100446572697661746987 -:100880006F6E20706174680052656A6563742069D8 -:100890006620796F75277265006E6F7420737572AC -:1008A0006500417070726F7665206465726976616B -:1008B00074696F6E00417070726F766500436F6E81 -:1008C0006669726D20746F6B656E004578706F722B -:1008D00074007075626C6963206B65793F005468C1 -:1008E00065206368616E6765207061746800697374 -:1008F00020756E757375616C004368616E67652065 -:100900007061746800546865207369676E20706157 -:100910007468005369676E207061746800556E7664 -:100920006572696669656420696E707574730055D7 -:10093000706461746500204C6564676572204C6961 -:100940007665006F722074686972642070617274D9 -:10095000790077616C6C657420736F66747761726F -:100960006500436F6E74696E7565005365677769DE -:100970007420706172736564206F6E63650A005540 -:100980004E4B4E4F574E0052455741524400457210 -:10099000726F72203A2046656573206E6F74206313 -:1009A0006F6E73697374656E74006F6D6E69004F5E -:1009B0004D4E4920005553445420004D4149442098 -:1009C000004F4D4E492061737365742025642000EB -:1009D00025640A0025730A00252E2A480041646414 -:1009E000726573732077617320616C7265616479DD -:1009F00020636865636B65640A00416D6F756E7492 -:100A0000206E6F74206D6174636865640A004164D0 -:100A10006472657373206E6F74206D6174636865B2 -:100A2000640A0046656573206973206E6F74206DDB -:100A30006174636865640A006F757470757420234F -:100A4000256400526176656E0048656C6C6F2066A7 -:100A5000726F6D206C697465636F696E0A0042691C -:100A600074636F696E00496E736964652061206C00 -:100A7000696272617279200A000000004F505F5273 -:100A8000455455524E0000004F505F43524541546B -:100A90004500000041535345542054414700000095 -:100AA000415353455420564552494649455200004A -:100AB000415353455420524553545249435445449D -:100AC00000000000526176656E636F696E006578A4 -:100AD00063657074696F6E5B25645D3A204C523DAE -:100AE0003078253038580A004552524F5200303184 -:100AF0003233343536373839616263646566303194 -:100B000032333435363738394142434445460000A4 -:100B100000000000000000000000000000000000D5 -:100B200000000000000000000000000000000000C5 -:100B300000000000000000000000000000000000B5 -:04000005C0D0000166 -:00000001FF diff --git a/tests/ravencoin-bin/app.sha256 b/tests/ravencoin-bin/app.sha256 deleted file mode 100644 index 0fd11859..00000000 --- a/tests/ravencoin-bin/app.sha256 +++ /dev/null @@ -1 +0,0 @@ -98e987810b7acaca8131aa657ebbce7b33297bf3cd9cf86d4d0b4d04347645fe diff --git a/tests/test_sign.py b/tests/test_sign.py index 227d0996..01cb2e21 100644 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -12,6 +12,18 @@ from bitcoin_client.exception import ConditionOfUseNotSatisfiedError from utils import automation +from typing import Tuple, List + +from ledgercomm import Transport + +from bitcoin_client.hwi.serialization import (CTransaction, CTxIn, CTxOut, COutPoint, + is_witness, is_p2wpkh, is_p2pkh, is_p2sh, hash160) +from bitcoin_client.hwi.bech32 import decode as bech32_decode +from bitcoin_client.hwi.base58 import decode as base58_decode +from bitcoin_client.utils import deser_trusted_input +from bitcoin_client.bitcoin_utils import bip143_digest, compress_pub_key +from bitcoin_client.bitcoin_cmd_builder import AddrType +from bitcoin_client.bitcoin_base_cmd import BitcoinBaseCommand def sign_from_json(cmd, filepath: Path): tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) @@ -89,4 +101,85 @@ def sign_from_json(cmd, filepath: Path): # sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") def test_sign(cmd): - sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file + + raw_utxos = [] # (raw utxo, idx of our vin) + sign_paths = [] # m/44'/175'... + lock_time = 0 + + # Parse the VINS + utxos: List[Tuple[CTransaction, int, int]] = [] + for raw_tx, output_index in raw_utxos: + utxo = CTransaction.from_bytes(raw_tx) + utxos.append((utxo, output_index)) + + # Sign our utxos + sign_pub_keys: List[bytes] = [] + for sign_path in sign_paths: + sign_pub_key, _, _ = cmd.get_public_key( + addr_type=AddrType.Legacy, + bip32_path=sign_path, + display=False + ) + sign_pub_keys.append(compress_pub_key(sign_pub_key)) + + # Get trusted inputs + inputs: List[Tuple[CTransaction, bytes]] = [ + (utxo, cmd.get_trusted_input(utxo=utxo, output_index=output_index)) + for utxo, output_index in utxos + ] + + # Create new tx + tx: CTransaction = CTransaction() + tx.nVersion = 2 + tx.nLockTime = lock_time + + # prepare vin + for i, (utxo, trusted_input) in enumerate(inputs): + if utxo.sha256 is None: + utxo.calc_sha256(with_witness=False) + + _, _, _, prev_txid, output_index, _, _ = deser_trusted_input(trusted_input) + assert prev_txid != utxo.sha256 + + script_pub_key: bytes = utxo.vout[output_index].scriptPubKey + tx.vin.append(CTxIn(outpoint=COutPoint(h=utxo.sha256, n=output_index), + scriptSig=script_pub_key, + nSequence=0xfffffffd)) + + # TODO: these + tx.vout.append(CTxOut(nValue=0, + scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvnt\x10SCAMCOINSCAMCOIN\x00\xa0rN\x18\t\x00\x00u')) + + tx.vout.append(CTxOut(nValue=0, + scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvno\x05TEST!u')) + + tx.vout.append(CTxOut(nValue=0, + scriptPubKey=bytes.fromhex('c014d4a4a095e02cd6a9b3cf15cf16cc42dc63baf3e006042342544301'))) + + for i in range(len(tx.vin)): + self.untrusted_hash_tx_input_start(tx=tx, + inputs=inputs, + input_index=i, + script=tx.vin[i].scriptSig, + is_new_transaction=(i == 0)) + + cmd.untrusted_hash_tx_input_finalize(tx=tx, + change_path=change_path) + + sigs: List[Tuple[bytes, bytes, Tuple[int, bytes]]] = [] + for i in range(len(tx.vin)): + cmd.untrusted_hash_tx_input_start(tx=tx, + inputs=[inputs[i]], + input_index=0, + script=tx.vin[i].scriptSig, + is_new_transaction=False) + _, _, amount = utxos[i] + sigs.append( + (bip143_digest(tx, amount, i), + sign_pub_keys[i], + cmd.untrusted_hash_sign(sign_path=sign_paths[i], + lock_time=tx.nLockTime, + sig_hash=1)) + ) + + print(sigs) From b96dcc698c6ade37d0410101e8f5b2a8a5bed944 Mon Sep 17 00:00:00 2001 From: kralverde <80051564+kralverde@users.noreply.github.com> Date: Mon, 21 Jun 2021 20:04:49 -0400 Subject: [PATCH 08/20] Update main.c --- src/main.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main.c b/src/main.c index edfdfe25..85b4b939 100644 --- a/src/main.c +++ b/src/main.c @@ -997,9 +997,6 @@ uint8_t prepare_single_output() { vars.tmp.fullAmount[textSize + str_len + 1] = '\0'; - PRINTF("%d\n", sizeof(vars.tmp.fullAmount)); - PRINTF("%s\n", vars.tmp.fullAmount); - PRINTF("%d\n", BIP44_COIN_TYPE); } return 1; From 45c129fc28510a960cef3fbd53c3505f96e83286 Mon Sep 17 00:00:00 2001 From: kralverde <80051564+kralverde@users.noreply.github.com> Date: Mon, 21 Jun 2021 20:07:15 -0400 Subject: [PATCH 09/20] Update btchip_helpers.c --- src/btchip_helpers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index 3e0e502b..f14f5a83 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -186,7 +186,7 @@ unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer if (final_op >= size || buffer[final_op] != 0x75) { return 0; } - while (script_ptr < final_op) { + while (script_ptr < final_op-6) { op = buffer[script_ptr++]; if (op == 0xC0) { if ((buffer[script_ptr+1] == 0x72) && From c5ccea135576cf3a91e5b7df39325282ae068e9c Mon Sep 17 00:00:00 2001 From: kralverde Date: Tue, 22 Jun 2021 11:25:18 -0400 Subject: [PATCH 10/20] More ravencoin work --- include/btchip_helpers.h | 2 + ledger-nanos-sdk | 1 + src/btchip_apdu_hash_input_finalize_full.c | 18 +++- src/btchip_display_variables.h | 2 +- src/btchip_helpers.c | 73 +++++++++++--- tests/.gitignore | 1 + tests/test_pubkey.py | 33 +++++-- tests/test_sign_message.py | 109 +++++++++++++++++++++ 8 files changed, 216 insertions(+), 23 deletions(-) create mode 160000 ledger-nanos-sdk create mode 100644 tests/test_sign_message.py diff --git a/include/btchip_helpers.h b/include/btchip_helpers.h index 36b006b0..195ebec2 100644 --- a/include/btchip_helpers.h +++ b/include/btchip_helpers.h @@ -31,7 +31,9 @@ #define OUTPUT_SCRIPT_NATIVE_WITNESS_PROGRAM_OFFSET 3 unsigned char btchip_output_script_is_regular(unsigned char *buffer); +unsigned char btchip_output_script_is_regular_ravencoin_asset(unsigned char *buffer); unsigned char btchip_output_script_is_p2sh(unsigned char *buffer); +unsigned char btchip_output_script_is_p2sh_ravencoin_asset(unsigned char *buffer); unsigned char btchip_output_script_is_op_return(unsigned char *buffer); unsigned char btchip_output_script_is_native_witness(unsigned char *buffer); diff --git a/ledger-nanos-sdk b/ledger-nanos-sdk new file mode 160000 index 00000000..30655d1b --- /dev/null +++ b/ledger-nanos-sdk @@ -0,0 +1 @@ +Subproject commit 30655d1b2aea4dbcf731e2697208d9fdcaa39782 diff --git a/src/btchip_apdu_hash_input_finalize_full.c b/src/btchip_apdu_hash_input_finalize_full.c index 0711ba30..49519b3b 100644 --- a/src/btchip_apdu_hash_input_finalize_full.c +++ b/src/btchip_apdu_hash_input_finalize_full.c @@ -64,7 +64,14 @@ static bool check_output_displayable() { isOpReturn = btchip_output_script_is_op_return(btchip_context_D.currentOutput + 8); - isP2sh = btchip_output_script_is_p2sh(btchip_context_D.currentOutput + 8); + + if (nullAmount && G_coin_config->kind==COIN_KIND_RAVENCOIN){ + isP2sh = btchip_output_script_is_p2sh_ravencoin_asset(btchip_context_D.currentOutput + 8); + } + else { + isP2sh = btchip_output_script_is_p2sh(btchip_context_D.currentOutput + 8); + } + isNativeSegwit = btchip_output_script_is_native_witness( btchip_context_D.currentOutput + 8); isOpCreate = @@ -74,6 +81,7 @@ static bool check_output_displayable() { btchip_output_script_is_op_call(btchip_context_D.currentOutput + 8, sizeof(btchip_context_D.currentOutput) - 8); isRavencoinAsset = + nullAmount && ((-1 != btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8)) || (btchip_output_script_get_ravencoin_asset_ptr(btchip_context_D.currentOutput + 8, sizeof(btchip_context_D.currentOutput) - 8, @@ -83,10 +91,12 @@ static bool check_output_displayable() { !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && !isP2sh && !(nullAmount && isOpReturn) && !isOpCreate && !isOpCall; } - else if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { + else if (nullAmount && G_coin_config->kind == COIN_KIND_RAVENCOIN) { + // Ravencoin assets only come into play when there is a null amount invalid_script = - !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && - !isP2sh && !(nullAmount && isOpReturn) && !(nullAmount && isRavencoinAsset); + !isRavencoinAsset && + !btchip_output_script_is_regular_ravencoin_asset(btchip_context_D.currentOutput + 8) && + !isP2sh && !isOpReturn; } else { invalid_script = diff --git a/src/btchip_display_variables.h b/src/btchip_display_variables.h index c434c027..6d0f0249 100644 --- a/src/btchip_display_variables.h +++ b/src/btchip_display_variables.h @@ -23,7 +23,7 @@ union display_variables { // char addressSummary[40]; // beginning of the output address ... end // of char fullAddress[65]; // the address - char fullAmount[32+1+17]; // Ravencoin: max asset length + space + max amt w/ decimal + char fullAmount[50]; // Ravencoin: max asset length (32) + space (1) + max amt w/ decimal (17) //char fullAmount[20]; // full amount char feesAmount[20]; // fees } tmp; diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index f14f5a83..fe27bd28 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -18,7 +18,8 @@ #include "btchip_internal.h" #include "btchip_apdu_constants.h" -const unsigned char TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE[] = {0x76, 0xA9, 0x14}; +const unsigned char TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE[] = {0x76, 0xA9, 0x14}; // w/o length +const unsigned char TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE[] = {0xA9, 0x14}; // w/o length const unsigned char TRANSACTION_OUTPUT_SCRIPT_PRE[] = { 0x19, 0x76, 0xA9, @@ -79,19 +80,23 @@ unsigned char btchip_output_script_is_regular(unsigned char *buffer) { return 1; } } - else if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { - if ((os_memcmp(buffer + 1, TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE, - sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE)) == 0) && - (os_memcmp(buffer + 1 + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE) + 20, + else { + if ((os_memcmp(buffer, TRANSACTION_OUTPUT_SCRIPT_PRE, + sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE)) == 0) && + (os_memcmp(buffer + sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE) + 20, TRANSACTION_OUTPUT_SCRIPT_POST, sizeof(TRANSACTION_OUTPUT_SCRIPT_POST)) == 0)) { return 1; } } - else { - if ((os_memcmp(buffer, TRANSACTION_OUTPUT_SCRIPT_PRE, - sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE)) == 0) && - (os_memcmp(buffer + sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE) + 20, + return 0; +} + +unsigned char btchip_output_script_is_regular_ravencoin_asset(unsigned char *buffer) { + if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { + if ((os_memcmp(buffer + 1, TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE, + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE)) == 0) && + (os_memcmp(buffer + 1 + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE) + 20, TRANSACTION_OUTPUT_SCRIPT_POST, sizeof(TRANSACTION_OUTPUT_SCRIPT_POST)) == 0)) { return 1; @@ -121,6 +126,19 @@ unsigned char btchip_output_script_is_p2sh(unsigned char *buffer) { return 0; } +unsigned char btchip_output_script_is_p2sh_ravencoin_asset(unsigned char *buffer) { + if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { + if ((os_memcmp(buffer + 1, TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE, + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE)) == 0) && + (os_memcmp(buffer + 1 + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE) + 20, + TRANSACTION_OUTPUT_SCRIPT_P2SH_POST, + sizeof(TRANSACTION_OUTPUT_SCRIPT_P2SH_POST)) == 0)) { + return 1; + } + } + return 0; +} + unsigned char btchip_output_script_is_native_witness(unsigned char *buffer) { if (G_coin_config->native_segwit_prefix) { if ((os_memcmp(buffer, TRANSACTION_OUTPUT_SCRIPT_P2WPKH_PRE, @@ -179,21 +197,54 @@ unsigned char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned cha } unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer, size_t size, int *ptr) { - unsigned int script_ptr = 1; //Skip the first pushdata op + // This method is also used in check_output_displayable and needs to ensure no overflows happen from bad scripts + unsigned int script_ptr = 1; // Skip the first pushdata op unsigned int op = -1; unsigned int final_op = buffer[0]; + unsigned char asset_len; if (final_op >= size || buffer[final_op] != 0x75) { return 0; } - while (script_ptr < final_op-6) { + while (script_ptr < final_op - 13) { // Definitely a bad asset script; too short op = buffer[script_ptr++]; if (op == 0xC0) { + // Verifying script if ((buffer[script_ptr+1] == 0x72) && (buffer[script_ptr+2] == 0x76) && (buffer[script_ptr+3] == 0x6E)) { + asset_len = buffer[script_ptr+5]; + if (asset_len > 32) { // Invalid script + return 0; + } + if (buffer[script_ptr+4] == 0x6F) { + // Ownership assets will not have an amount + if (script_ptr+5+asset_len >= final_op) { + return 0; // Too small + } + } + else { + if (script_ptr+5+asset_len+8 >= final_op) { + return 0; // Too small + } + } *ptr = script_ptr + 4; } else { + asset_len = buffer[script_ptr+6]; + if (asset_len > 32) { // Invalid script + return 0; + } + if (buffer[script_ptr+5] == 0x6F) { + // Ownership assets will not have an amount + if (script_ptr+6+asset_len >= final_op) { + return 0; // Too small + } + } + else { + if (script_ptr+6+asset_len+8 >= final_op) { + return 0; // Too small + } + } *ptr = script_ptr + 5; } return 1; diff --git a/tests/.gitignore b/tests/.gitignore index 3936d581..a545cd8c 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,2 +1,3 @@ bitcoin-bin bitcoin-testnet-bin +ravencoin-bin diff --git a/tests/test_pubkey.py b/tests/test_pubkey.py index 9b1c142c..36cfbe87 100644 --- a/tests/test_pubkey.py +++ b/tests/test_pubkey.py @@ -3,10 +3,29 @@ def test_get_public_key(cmd): # legacy address - #pub_key, addr, bip32_chain_code = cmd.get_public_key( - # addr_type=AddrType.Legacy, - # bip32_path="m/44'/175'/0'/0/0", - # display=False - #) - #print(addr) - pass \ No newline at end of file + + paths =[ + "m/44'/175'/0'/0/0", + "m/44'/175'/0'/0/1", + "m/44'/175'/0'/0/2", + "m/44'/175'/0'/0/3", + "m/44'/175'/0'/0/4", + "m/44'/175'/0'/1/0", + "m/44'/175'/0'/1/1", + "m/44'/175'/0'/1/2", + "m/44'/175'/0'/1/3", + "m/44'/175'/0'/1/4", + ] + + addrs = [] + + for path in paths: + pub_key, addr, bip32_chain_code = cmd.get_public_key( + addr_type=AddrType.Legacy, + bip32_path=path, + display=False + ) + addrs.append(addr) + + print("ADDRESSES:") + print(addrs) \ No newline at end of file diff --git a/tests/test_sign_message.py b/tests/test_sign_message.py new file mode 100644 index 00000000..8477ca56 --- /dev/null +++ b/tests/test_sign_message.py @@ -0,0 +1,109 @@ +import re +import base64 +import hashlib +from bitcoin_client.bitcoin_base_cmd import AddrType + +def test_sign_message(cmd): + message_og = 'Test' + path_og = "m/44'/175'/0'/0/0" + + # From https://github.com/LedgerHQ/btchip-python/blob/master/btchip/btchip.py + + # Prepare signing + + def writeUint32BE(value, buffer): + buffer.append((value >> 24) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append(value & 0xff) + return buffer + + def parse_bip32_path(path): + if len(path) == 0: + return bytearray([ 0 ]) + result = [] + elements = path.split('/') + if len(elements) > 10: + raise Exception("Path too long") + for pathElement in elements: + element = re.split('\'|h|H', pathElement) + if len(element) == 1: + writeUint32BE(int(element[0]), result) + else: + writeUint32BE(0x80000000 | int(element[0]), result) + return bytearray([ len(elements) ] + result) + + path = parse_bip32_path(path_og[2:]) + message = message_og.encode('utf8') + + result = {} + offset = 0 + encryptedOutputData = b"" + while (offset < len(message)): + params = []; + if offset == 0: + params.extend(path) + params.append((len(message) >> 8) & 0xff) + params.append(len(message) & 0xff) + p2 = 0x01 + else: + p2 = 0x80 + blockLength = 255 - len(params) + if ((offset + blockLength) < len(message)): + dataLength = blockLength + else: + dataLength = len(message) - offset + params.extend(bytearray(message[offset : offset + dataLength])) + apdu = [ 0xe0, 0x4e, 0x00, p2 ] + apdu.append(len(params)) + apdu.extend(params) + _, response = cmd.transport.exchange_raw(bytearray(apdu)) + encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] + offset += blockLength + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + result['secureScreenData'] = response[offset:] + result['encryptedOutputData'] = encryptedOutputData + + # Sign + + print('Message Hash') + print(hashlib.sha256(message).hexdigest().upper()) + + apdu = [ 0xe0, 0x4e, 0x80, 0x00 ] + params = [] + params.append(0x00) + apdu.append(len(params)) + apdu.extend(params) + _, signature = cmd.transport.exchange_raw(bytearray(apdu)) + + # Parse the ASN.1 signature + rLength = signature[3] + r = signature[4: 4 + rLength] + sLength = signature[4 + rLength + 1] + s = signature[4 + rLength + 2:] + if rLength == 33: + r = r[1:] + if sLength == 33: + s = s[1:] + # And convert it + + # Pad r and s points with 0x00 bytes when the point is small to get valid signature. + r_padded = bytes([0x00]) * (32 - len(r)) + r + s_padded = bytes([0x00]) * (32 - len(s)) + s + + p = bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded + pub_key, addr, bip32_chain_code = cmd.get_public_key( + addr_type=AddrType.Legacy, + bip32_path=path_og, + display=False + ) + readable_sig = base64.b64encode(p).decode('ascii') + print(addr) + print(message_og) + print(readable_sig) + + assert 'INyo6gzuMMY9wNvT71+amLPG+zBnL4PO8leCdYvSZGuLaVpvHrFcDFf3Q9Gt0ReRuwIxUSaKa+SGFJoxc8b32Zo='==\ + readable_sig From a3f1294bdcedc401ad66dc43f749b7847d3de009 Mon Sep 17 00:00:00 2001 From: kralverde Date: Tue, 22 Jun 2021 18:31:35 -0400 Subject: [PATCH 11/20] Changed display var size --- src/btchip_display_variables.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/btchip_display_variables.h b/src/btchip_display_variables.h index 6d0f0249..d6ea3f1c 100644 --- a/src/btchip_display_variables.h +++ b/src/btchip_display_variables.h @@ -23,7 +23,8 @@ union display_variables { // char addressSummary[40]; // beginning of the output address ... end // of char fullAddress[65]; // the address - char fullAmount[50]; // Ravencoin: max asset length (32) + space (1) + max amt w/ decimal (17) + //Ravencoin: max asset length (32) + space (1) + max amt whole (11) + decimal (1) + max amt decimal (8) + \0 (1) + char fullAmount[54]; //char fullAmount[20]; // full amount char feesAmount[20]; // fees } tmp; From 5a7737986ef85bfb14687ed7870c975ec32668d9 Mon Sep 17 00:00:00 2001 From: kralverde Date: Wed, 23 Jun 2021 16:25:17 -0400 Subject: [PATCH 12/20] tweaks and trying to get testing working --- tests/bitcoin_client/bitcoin_cmd.py | 14 +--- tests/data/one-to-one/p2pkh/tx.json | 16 ++-- tests/test_pubkey.py | 3 +- tests/test_sign.py | 109 ++-------------------------- 4 files changed, 18 insertions(+), 124 deletions(-) diff --git a/tests/bitcoin_client/bitcoin_cmd.py b/tests/bitcoin_client/bitcoin_cmd.py index 4d9859db..c32fc37d 100644 --- a/tests/bitcoin_client/bitcoin_cmd.py +++ b/tests/bitcoin_client/bitcoin_cmd.py @@ -116,12 +116,11 @@ def sign_new_tx(self, hash160(sign_pub_keys[i]) + # hash160(pubkey) b"\x88" + # OP_EQUALVERIFY b"\xac") # OP_CHECKSIG - print(script_pub_key) tx.vin.append(CTxIn(outpoint=COutPoint(h=utxo.sha256, n=output_index), scriptSig=script_pub_key, nSequence=0xfffffffd)) - if False and amount_available - fees > amount: + if amount_available - fees > amount: change_pub_key, _, _ = self.get_public_key( addr_type=AddrType.Legacy, bip32_path=change_path, @@ -170,7 +169,7 @@ def sign_new_tx(self, base58_decode(address)[1:-4] + # hash160(redeem_script) b"\x87") # OP_EQUAL # P2PKH address (mainnet and testnet) - elif address.startswith("1") or (address.startswith("m") or address.startswith("n")): + elif address.startswith("R") or (address.startswith("m") or address.startswith("n")): script_pub_key = (b"\x76" + # OP_DUP b"\xa9" + # OP_HASH160 b"\x14" + # bytes to push (20) @@ -181,14 +180,7 @@ def sign_new_tx(self, raise Exception(f"Unsupported address: '{address}'") tx.vout.append(CTxOut(nValue=amount, - scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvnt\x10SCAMCOINSCAMCOIN\x00\xa0rN\x18\t\x00\x00u')) - - tx.vout.append(CTxOut(nValue=0, - scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvno\x05TEST!u')) - - tx.vout.append(CTxOut(nValue=0, - scriptPubKey=bytes.fromhex('c014d4a4a095e02cd6a9b3cf15cf16cc42dc63baf3e006042342544301'))) - + scriptPubKey=script_pub_key)) for i in range(len(tx.vin)): self.untrusted_hash_tx_input_start(tx=tx, diff --git a/tests/data/one-to-one/p2pkh/tx.json b/tests/data/one-to-one/p2pkh/tx.json index 788fcbff..e934c1df 100644 --- a/tests/data/one-to-one/p2pkh/tx.json +++ b/tests/data/one-to-one/p2pkh/tx.json @@ -1,18 +1,16 @@ { - "txid": "e65ddd4292d7a2f8e52d7c4a8de3ddbda2448e020b6107d93880aa082c6bbcf1", - "raw": "0200000003cdcd1988112215922607a7d25e85315e152f5adfcfdcbe702d4a88a805b92365490000006b483045022100a37db1e91cfe40faa852fbb1e46c35b00f1c42e7fa6b592f590e1ea257483cb60220344d5050c1bf94408bfed67d7e54c91b3067e986e51d3d8f77b834140ea181e4012102ce7896973ed3d6d924312d1aafae39dc6ed8efdd5154bbae070bc28ae0bf6376feffffffd32ec2d64ce2d330ac2bfcbd6bdb9e5f2b454fe51c0423d111a27e719ffc49f2010000006a4730440220061e9dab97e20572fa907052934194d8543cf6b89a664bb9593e2db62a5667ad02207e7291f64e66323dda5305588ec3e8959e3e3980f1465367389f46a01ade21c80121036e5bc605676ab36842a987b74f38ccfa474a09083117d0971a5c4698ba0ce0d2feffffffb477f5b27fef53657020ede50212b5aa7178e1b240e14d531fd03ff97a224e1d2b0000006b483045022100e553618a63bb883efab68850cc13ca1a945c60f4c50d390287db6e071d224c9e02204e5acd4a61132de1e019cb750e18b6a1ef64ca92a60ddb27dc8d7bededf4961e0121028fd17a52d8d4713ec80f460aebe7e8a4dd64ccf04289dc10c6aec932811d6577feffffff028b421200000000001976a9143b5bd63dea6fc743f31f366b678f198581ce8edc88ac00000000000000003176a914adde097ccf77eac3712dbc9209478d20fcc390bd88acc01572766e74085343414d434f494e00e40b540200000075b2971b00", - "amount": 999800, - "fees": 200, - "to": "mhKsh7EzJo1gSU1vrpyejS1qsJAuKyaWWg", + "amount": 10200000000, + "fees": 14906044, + "to": "RFkGFbRtzs6YLRaCTiNpMzaqrirv6noVh9", "sign_paths": ["m/44'/175'/0'/0/0"], "change_path": null, "lock_time": 1901594, "utxos": [ { - "txid": "e65ddd4292d7a2f8e52d7c4a8de3ddbda2448e020b6107d93880aa082c6bbcf1", - "raw": "0200000003cdcd1988112215922607a7d25e85315e152f5adfcfdcbe702d4a88a805b92365490000006b483045022100a37db1e91cfe40faa852fbb1e46c35b00f1c42e7fa6b592f590e1ea257483cb60220344d5050c1bf94408bfed67d7e54c91b3067e986e51d3d8f77b834140ea181e4012102ce7896973ed3d6d924312d1aafae39dc6ed8efdd5154bbae070bc28ae0bf6376feffffffd32ec2d64ce2d330ac2bfcbd6bdb9e5f2b454fe51c0423d111a27e719ffc49f2010000006a4730440220061e9dab97e20572fa907052934194d8543cf6b89a664bb9593e2db62a5667ad02207e7291f64e66323dda5305588ec3e8959e3e3980f1465367389f46a01ade21c80121036e5bc605676ab36842a987b74f38ccfa474a09083117d0971a5c4698ba0ce0d2feffffffb477f5b27fef53657020ede50212b5aa7178e1b240e14d531fd03ff97a224e1d2b0000006b483045022100e553618a63bb883efab68850cc13ca1a945c60f4c50d390287db6e071d224c9e02204e5acd4a61132de1e019cb750e18b6a1ef64ca92a60ddb27dc8d7bededf4961e0121028fd17a52d8d4713ec80f460aebe7e8a4dd64ccf04289dc10c6aec932811d6577feffffff028b421200000000001976a9143b5bd63dea6fc743f31f366b678f198581ce8edc88ac00000000000000003176a914adde097ccf77eac3712dbc9209478d20fcc390bd88acc01572766e74085343414d434f494e00e40b540200000075b2971b00", - "output_indexes": [1], - "output_amounts": [999800] + "txid": "0b6e055fde23f5e0b5070cead011ac7d847717aa8a4a91bddadb24e609d336fd", + "raw": "02000000013e0b1f94e33b7bc86699e5906aacbc01ca17c4a632e107c5d1561ada157c44f4010000006b483045022100af3f5129d0d78b0684b13f469ef6426430ba49faed21b86b1d5eada404f8725502202ece7659e98f6a4239cb6d3685b88def6c5e120a67235f405a85af131a5d63280121035e6655bb66d3ec8612d3fc7e6656387e085fa3dc26d3ce3f391baccda011c037feffffff02bc18db60020000001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188aca7d6aae8520000001976a91446ef7841d380f258ae1f95f264d4532f93cb28e588ac18a31b00", + "output_indexes": [0], + "output_amounts": [10214906044] } ] diff --git a/tests/test_pubkey.py b/tests/test_pubkey.py index 36cfbe87..351b709c 100644 --- a/tests/test_pubkey.py +++ b/tests/test_pubkey.py @@ -1,4 +1,5 @@ from bitcoin_client.bitcoin_base_cmd import AddrType +from bitcoin_client.hwi.base58 import decode as base58_decode def test_get_public_key(cmd): @@ -25,7 +26,7 @@ def test_get_public_key(cmd): bip32_path=path, display=False ) - addrs.append(addr) + addrs.append((addr, base58_decode(addr)[1:21].hex())) print("ADDRESSES:") print(addrs) \ No newline at end of file diff --git a/tests/test_sign.py b/tests/test_sign.py index 01cb2e21..094e5eb1 100644 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -12,19 +12,6 @@ from bitcoin_client.exception import ConditionOfUseNotSatisfiedError from utils import automation -from typing import Tuple, List - -from ledgercomm import Transport - -from bitcoin_client.hwi.serialization import (CTransaction, CTxIn, CTxOut, COutPoint, - is_witness, is_p2wpkh, is_p2pkh, is_p2sh, hash160) -from bitcoin_client.hwi.bech32 import decode as bech32_decode -from bitcoin_client.hwi.base58 import decode as base58_decode -from bitcoin_client.utils import deser_trusted_input -from bitcoin_client.bitcoin_utils import bip143_digest, compress_pub_key -from bitcoin_client.bitcoin_cmd_builder import AddrType -from bitcoin_client.bitcoin_base_cmd import BitcoinBaseCommand - def sign_from_json(cmd, filepath: Path): tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) @@ -89,97 +76,13 @@ def sign_from_json(cmd, filepath: Path): # sign_from_json(cmd, filepath) -#@automation("automations/accept.json") -#def test_sign_p2pkh_accept(cmd): -# for filepath in Path("data").rglob("p2pkh/tx.json"): -# sign_from_json(cmd, filepath) - +@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') #@automation("automations/reject.json") #def test_sign_fail_p2pkh_reject(cmd): # with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") - -def test_sign(cmd): - - raw_utxos = [] # (raw utxo, idx of our vin) - sign_paths = [] # m/44'/175'... - lock_time = 0 - - # Parse the VINS - utxos: List[Tuple[CTransaction, int, int]] = [] - for raw_tx, output_index in raw_utxos: - utxo = CTransaction.from_bytes(raw_tx) - utxos.append((utxo, output_index)) - - # Sign our utxos - sign_pub_keys: List[bytes] = [] - for sign_path in sign_paths: - sign_pub_key, _, _ = cmd.get_public_key( - addr_type=AddrType.Legacy, - bip32_path=sign_path, - display=False - ) - sign_pub_keys.append(compress_pub_key(sign_pub_key)) - - # Get trusted inputs - inputs: List[Tuple[CTransaction, bytes]] = [ - (utxo, cmd.get_trusted_input(utxo=utxo, output_index=output_index)) - for utxo, output_index in utxos - ] - - # Create new tx - tx: CTransaction = CTransaction() - tx.nVersion = 2 - tx.nLockTime = lock_time - - # prepare vin - for i, (utxo, trusted_input) in enumerate(inputs): - if utxo.sha256 is None: - utxo.calc_sha256(with_witness=False) - - _, _, _, prev_txid, output_index, _, _ = deser_trusted_input(trusted_input) - assert prev_txid != utxo.sha256 - - script_pub_key: bytes = utxo.vout[output_index].scriptPubKey - tx.vin.append(CTxIn(outpoint=COutPoint(h=utxo.sha256, n=output_index), - scriptSig=script_pub_key, - nSequence=0xfffffffd)) - - # TODO: these - tx.vout.append(CTxOut(nValue=0, - scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvnt\x10SCAMCOINSCAMCOIN\x00\xa0rN\x18\t\x00\x00u')) - - tx.vout.append(CTxOut(nValue=0, - scriptPubKey=b'v\xa9\x14\xad\xde\t|\xcfw\xea\xc3q-\xbc\x92\tG\x8d \xfc\xc3\x90\xbd\x88\xac\xc0\x15rvno\x05TEST!u')) - - tx.vout.append(CTxOut(nValue=0, - scriptPubKey=bytes.fromhex('c014d4a4a095e02cd6a9b3cf15cf16cc42dc63baf3e006042342544301'))) - - for i in range(len(tx.vin)): - self.untrusted_hash_tx_input_start(tx=tx, - inputs=inputs, - input_index=i, - script=tx.vin[i].scriptSig, - is_new_transaction=(i == 0)) - - cmd.untrusted_hash_tx_input_finalize(tx=tx, - change_path=change_path) - - sigs: List[Tuple[bytes, bytes, Tuple[int, bytes]]] = [] - for i in range(len(tx.vin)): - cmd.untrusted_hash_tx_input_start(tx=tx, - inputs=[inputs[i]], - input_index=0, - script=tx.vin[i].scriptSig, - is_new_transaction=False) - _, _, amount = utxos[i] - sigs.append( - (bip143_digest(tx, amount, i), - sign_pub_keys[i], - cmd.untrusted_hash_sign(sign_path=sign_paths[i], - lock_time=tx.nLockTime, - sig_hash=1)) - ) - - print(sigs) +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file From 359c1f0e528d8a279be5d11fd573375b5989b3e5 Mon Sep 17 00:00:00 2001 From: kralverde <80051564+kralverde@users.noreply.github.com> Date: Wed, 23 Jun 2021 16:29:26 -0400 Subject: [PATCH 13/20] Update btchip_helpers.c --- src/btchip_helpers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index fe27bd28..d9966aa0 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -206,7 +206,7 @@ unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer if (final_op >= size || buffer[final_op] != 0x75) { return 0; } - while (script_ptr < final_op - 13) { // Definitely a bad asset script; too short + while (script_ptr < final_op - 7) { // Definitely a bad asset script; too short op = buffer[script_ptr++]; if (op == 0xC0) { // Verifying script From ed35c852afaeeb14a33ac07adb44a55b34631ee0 Mon Sep 17 00:00:00 2001 From: kralverde Date: Fri, 25 Jun 2021 14:35:32 -0400 Subject: [PATCH 14/20] Tests that 'work' --- tests/btchippython/.gitignore | 2 + tests/btchippython/.gitmodules | 0 tests/btchippython/LICENSE | 202 +++++ tests/btchippython/MANIFEST.in | 3 + tests/btchippython/README.md | 37 + tests/btchippython/btchip/__init__.py | 20 + .../btchippython/btchip/bitcoinTransaction.py | 165 ++++ tests/btchippython/btchip/bitcoinVarint.py | 63 ++ tests/btchippython/btchip/btchip.py | 712 ++++++++++++++++++ tests/btchippython/btchip/btchipComm.py | 254 +++++++ tests/btchippython/btchip/btchipException.py | 28 + .../btchip/btchipFirmwareWizard.py | 24 + tests/btchippython/btchip/btchipHelpers.py | 86 +++ .../btchippython/btchip/btchipKeyRecovery.py | 57 ++ .../btchippython/btchip/btchipPersoWizard.py | 376 +++++++++ tests/btchippython/btchip/btchipUtils.py | 105 +++ tests/btchippython/btchip/ledgerWrapper.py | 92 +++ tests/btchippython/btchip/msqr.py | 94 +++ tests/btchippython/btchip/ui/__init__.py | 19 + .../btchip/ui/personalization00start.py | 55 ++ .../btchip/ui/personalization01seed.py | 77 ++ .../btchip/ui/personalization02security.py | 96 +++ .../btchip/ui/personalization03config.py | 68 ++ .../btchip/ui/personalization04finalize.py | 63 ++ .../btchip/ui/personalizationseedbackup01.py | 71 ++ .../btchip/ui/personalizationseedbackup02.py | 46 ++ .../btchip/ui/personalizationseedbackup03.py | 76 ++ .../btchip/ui/personalizationseedbackup04.py | 50 ++ .../samples/getFirmwareVersion.py | 26 + tests/btchippython/samples/runScript.py | 54 ++ tests/btchippython/setup.py | 31 + .../coinkite-cosigning/testGetPublicKeyDev.js | 115 +++ .../tests/coinkite-cosigning/testSignDev.js | 168 +++++ tests/btchippython/tests/testConnectivity.py | 34 + .../tests/testMessageSignature.py | 56 ++ .../btchippython/tests/testMultisigArmory.py | 194 +++++ .../tests/testMultisigArmoryNo2FA.py | 169 +++++ .../tests/testSimpleTransaction.py | 78 ++ tests/btchippython/ui/make.sh | 12 + .../ui/personalization-00-start.ui | 98 +++ .../ui/personalization-01-seed.ui | 160 ++++ .../ui/personalization-02-security.ui | 226 ++++++ .../ui/personalization-03-config.ui | 136 ++++ .../ui/personalization-04-finalize.ui | 122 +++ .../ui/personalization-seedbackup-01.ui | 138 ++++ .../ui/personalization-seedbackup-02.ui | 69 ++ .../ui/personalization-seedbackup-03.ui | 165 ++++ .../ui/personalization-seedbackup-04.ui | 82 ++ tests/electrum_clone/electrumravencoin | 1 + tests/electrum_clone/ledger_sign_funcs.py | 84 +++ tests/test_pubkey.py | 2 +- .../{test_sign.py => test_sign_asset_1_1.py} | 40 +- tests/test_sign_rvn_1_1.py | 124 +++ 53 files changed, 5322 insertions(+), 3 deletions(-) create mode 100644 tests/btchippython/.gitignore create mode 100644 tests/btchippython/.gitmodules create mode 100644 tests/btchippython/LICENSE create mode 100644 tests/btchippython/MANIFEST.in create mode 100644 tests/btchippython/README.md create mode 100644 tests/btchippython/btchip/__init__.py create mode 100644 tests/btchippython/btchip/bitcoinTransaction.py create mode 100644 tests/btchippython/btchip/bitcoinVarint.py create mode 100644 tests/btchippython/btchip/btchip.py create mode 100644 tests/btchippython/btchip/btchipComm.py create mode 100644 tests/btchippython/btchip/btchipException.py create mode 100644 tests/btchippython/btchip/btchipFirmwareWizard.py create mode 100644 tests/btchippython/btchip/btchipHelpers.py create mode 100644 tests/btchippython/btchip/btchipKeyRecovery.py create mode 100644 tests/btchippython/btchip/btchipPersoWizard.py create mode 100644 tests/btchippython/btchip/btchipUtils.py create mode 100644 tests/btchippython/btchip/ledgerWrapper.py create mode 100644 tests/btchippython/btchip/msqr.py create mode 100644 tests/btchippython/btchip/ui/__init__.py create mode 100644 tests/btchippython/btchip/ui/personalization00start.py create mode 100644 tests/btchippython/btchip/ui/personalization01seed.py create mode 100644 tests/btchippython/btchip/ui/personalization02security.py create mode 100644 tests/btchippython/btchip/ui/personalization03config.py create mode 100644 tests/btchippython/btchip/ui/personalization04finalize.py create mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup01.py create mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup02.py create mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup03.py create mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup04.py create mode 100644 tests/btchippython/samples/getFirmwareVersion.py create mode 100644 tests/btchippython/samples/runScript.py create mode 100644 tests/btchippython/setup.py create mode 100644 tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js create mode 100644 tests/btchippython/tests/coinkite-cosigning/testSignDev.js create mode 100644 tests/btchippython/tests/testConnectivity.py create mode 100644 tests/btchippython/tests/testMessageSignature.py create mode 100644 tests/btchippython/tests/testMultisigArmory.py create mode 100644 tests/btchippython/tests/testMultisigArmoryNo2FA.py create mode 100644 tests/btchippython/tests/testSimpleTransaction.py create mode 100755 tests/btchippython/ui/make.sh create mode 100644 tests/btchippython/ui/personalization-00-start.ui create mode 100644 tests/btchippython/ui/personalization-01-seed.ui create mode 100644 tests/btchippython/ui/personalization-02-security.ui create mode 100644 tests/btchippython/ui/personalization-03-config.ui create mode 100644 tests/btchippython/ui/personalization-04-finalize.ui create mode 100644 tests/btchippython/ui/personalization-seedbackup-01.ui create mode 100644 tests/btchippython/ui/personalization-seedbackup-02.ui create mode 100644 tests/btchippython/ui/personalization-seedbackup-03.ui create mode 100644 tests/btchippython/ui/personalization-seedbackup-04.ui create mode 160000 tests/electrum_clone/electrumravencoin create mode 100644 tests/electrum_clone/ledger_sign_funcs.py rename tests/{test_sign.py => test_sign_asset_1_1.py} (61%) create mode 100644 tests/test_sign_rvn_1_1.py diff --git a/tests/btchippython/.gitignore b/tests/btchippython/.gitignore new file mode 100644 index 00000000..2f78cf5b --- /dev/null +++ b/tests/btchippython/.gitignore @@ -0,0 +1,2 @@ +*.pyc + diff --git a/tests/btchippython/.gitmodules b/tests/btchippython/.gitmodules new file mode 100644 index 00000000..e69de29b diff --git a/tests/btchippython/LICENSE b/tests/btchippython/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/tests/btchippython/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tests/btchippython/MANIFEST.in b/tests/btchippython/MANIFEST.in new file mode 100644 index 00000000..3eb1cb4b --- /dev/null +++ b/tests/btchippython/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE + diff --git a/tests/btchippython/README.md b/tests/btchippython/README.md new file mode 100644 index 00000000..3d71f575 --- /dev/null +++ b/tests/btchippython/README.md @@ -0,0 +1,37 @@ +btchip-python +============= + +Python communication library for Ledger Hardware Wallet products + +Requirements +------------- + +This API is available on pip - install with pip install btchip-python + +Building on a Unix platform requires libusb-1.0-0-dev and libudev-dev installed previously + +Interim Debian packages have also been built by Richard Ulrich at https://launchpad.net/~richi-paraeasy/+archive/ubuntu/bitcoin/ (btchip-python, hidapi and python-hidapi) + +For optional BIP 39 support during dongle setup, also install https://github.com/trezor/python-mnemonic - also available as a Debian package at the previous link (python-mnemonic) + +Building on Windows +-------------------- + + - Download and install the latest Python 2.7 version from https://www.python.org/downloads/windows/ + - Install Microsoft Visual C++ Compiler for Python 2.7 from http://www.microsoft.com/en-us/download/details.aspx?id=44266 + - Download and install PyQt4 for Python 2.7 from https://www.riverbankcomputing.com/software/pyqt/download + - Install the btchip library (open a command prompt and enter c:\python27\scripts\pip install btchip) + +Building/Installing on FreeBSD +------------------------------ + +On FreeBSD you can install the packages: + + pkg install security/py-btchip-python + +or build via ports: + + cd /usr/ports/security/py-btchip-python + make install clean + + diff --git a/tests/btchippython/btchip/__init__.py b/tests/btchippython/btchip/__init__.py new file mode 100644 index 00000000..9e0789e1 --- /dev/null +++ b/tests/btchippython/btchip/__init__.py @@ -0,0 +1,20 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" +__version__ = "0.1.32" + diff --git a/tests/btchippython/btchip/bitcoinTransaction.py b/tests/btchippython/btchip/bitcoinTransaction.py new file mode 100644 index 00000000..35276e9e --- /dev/null +++ b/tests/btchippython/btchip/bitcoinTransaction.py @@ -0,0 +1,165 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .bitcoinVarint import * +from binascii import hexlify + +class bitcoinInput: + + def __init__(self, bufferOffset=None): + self.prevOut = "" + self.script = "" + self.sequence = "" + if bufferOffset is not None: + buf = bufferOffset['buffer'] + offset = bufferOffset['offset'] + self.prevOut = buf[offset:offset + 36] + offset += 36 + scriptSize = readVarint(buf, offset) + offset += scriptSize['size'] + self.script = buf[offset:offset + scriptSize['value']] + offset += scriptSize['value'] + self.sequence = buf[offset:offset + 4] + offset += 4 + bufferOffset['offset'] = offset + + def serialize(self): + result = [] + result.extend(self.prevOut) + writeVarint(len(self.script), result) + result.extend(self.script) + result.extend(self.sequence) + return result + + def __str__(self): + buf = "Prevout : " + hexlify(self.prevOut) + "\r\n" + buf += "Script : " + hexlify(self.script) + "\r\n" + buf += "Sequence : " + hexlify(self.sequence) + "\r\n" + return buf + +class bitcoinOutput: + + def __init__(self, bufferOffset=None): + self.amount = "" + self.script = "" + if bufferOffset is not None: + buf = bufferOffset['buffer'] + offset = bufferOffset['offset'] + self.amount = buf[offset:offset + 8] + offset += 8 + scriptSize = readVarint(buf, offset) + offset += scriptSize['size'] + self.script = buf[offset:offset + scriptSize['value']] + offset += scriptSize['value'] + bufferOffset['offset'] = offset + + def serialize(self): + result = [] + result.extend(self.amount) + writeVarint(len(self.script), result) + result.extend(self.script) + return result + + def __str__(self): + buf = "Amount : " + hexlify(self.amount) + "\r\n" + buf += "Script : " + hexlify(self.script) + "\r\n" + return buf + + +class bitcoinTransaction: + + def __init__(self, data=None): + self.version = "" + self.inputs = [] + self.outputs = [] + self.lockTime = "" + self.witness = False + self.witnessScript = "" + if data is not None: + offset = 0 + self.version = data[offset:offset + 4] + offset += 4 + if (data[offset] == 0) and (data[offset + 1] != 0): + offset += 2 + self.witness = True + inputSize = readVarint(data, offset) + offset += inputSize['size'] + numInputs = inputSize['value'] + for i in range(numInputs): + tmp = { 'buffer': data, 'offset' : offset} + self.inputs.append(bitcoinInput(tmp)) + offset = tmp['offset'] + outputSize = readVarint(data, offset) + offset += outputSize['size'] + numOutputs = outputSize['value'] + for i in range(numOutputs): + tmp = { 'buffer': data, 'offset' : offset} + self.outputs.append(bitcoinOutput(tmp)) + offset = tmp['offset'] + if self.witness: + self.witnessScript = data[offset : len(data) - 4] + self.lockTime = data[len(data) - 4:] + else: + self.lockTime = data[offset:offset + 4] + + def serialize(self, skipOutputLocktime=False, skipWitness=False): + if skipWitness or (not self.witness): + useWitness = False + else: + useWitness = True + result = [] + result.extend(self.version) + if useWitness: + result.append(0x00) + result.append(0x01) + writeVarint(len(self.inputs), result) + for trinput in self.inputs: + result.extend(trinput.serialize()) + if not skipOutputLocktime: + writeVarint(len(self.outputs), result) + for troutput in self.outputs: + result.extend(troutput.serialize()) + if useWitness: + result.extend(self.witnessScript) + result.extend(self.lockTime) + return result + + def serializeOutputs(self): + result = [] + writeVarint(len(self.outputs), result) + for troutput in self.outputs: + result.extend(troutput.serialize()) + return result + + def __str__(self): + buf = "Version : " + hexlify(self.version) + "\r\n" + index = 1 + for trinput in self.inputs: + buf += "Input #" + str(index) + "\r\n" + buf += str(trinput) + index+=1 + index = 1 + for troutput in self.outputs: + buf += "Output #" + str(index) + "\r\n" + buf += str(troutput) + index+=1 + buf += "Locktime : " + hexlify(self.lockTime) + "\r\n" + if self.witness: + buf += "Witness script : " + hexlify(self.witnessScript) + "\r\n" + return buf diff --git a/tests/btchippython/btchip/bitcoinVarint.py b/tests/btchippython/btchip/bitcoinVarint.py new file mode 100644 index 00000000..a0dd8680 --- /dev/null +++ b/tests/btchippython/btchip/bitcoinVarint.py @@ -0,0 +1,63 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .btchipException import BTChipException + +def readVarint(buffer, offset): + varintSize = 0 + value = 0 + if (buffer[offset] < 0xfd): + value = buffer[offset] + varintSize = 1 + elif (buffer[offset] == 0xfd): + value = (buffer[offset + 2] << 8) | (buffer[offset + 1]) + varintSize = 3 + elif (buffer[offset] == 0xfe): + value = (buffer[offset + 4] << 24) | (buffer[offset + 3] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 1]) + varintSize = 5 + else: + raise BTChipException("unsupported varint") + return { "value": value, "size": varintSize } + +def writeVarint(value, buffer): + if (value < 0xfd): + buffer.append(value) + elif (value <= 0xffff): + buffer.append(0xfd) + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + elif (value <= 0xffffffff): + buffer.append(0xfe) + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 24) & 0xff) + else: + raise BTChipException("unsupported encoding") + return buffer + +def getVarintSize(value): + if (value < 0xfd): + return 1 + elif (value <= 0xffff): + return 3 + elif (value <= 0xffffffff): + return 5 + else: + raise BTChipException("unsupported encoding") diff --git a/tests/btchippython/btchip/btchip.py b/tests/btchippython/btchip/btchip.py new file mode 100644 index 00000000..11e411fb --- /dev/null +++ b/tests/btchippython/btchip/btchip.py @@ -0,0 +1,712 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .btchipComm import * +from .bitcoinTransaction import * +from .bitcoinVarint import * +from .btchipException import * +from .btchipHelpers import * +from .btchipKeyRecovery import * +from binascii import hexlify, unhexlify + +class btchip: + BTCHIP_CLA = 0xe0 + BTCHIP_JC_EXT_CLA = 0xf0 + + BTCHIP_INS_SET_ALTERNATE_COIN_VERSION = 0x14 + BTCHIP_INS_SETUP = 0x20 + BTCHIP_INS_VERIFY_PIN = 0x22 + BTCHIP_INS_GET_OPERATION_MODE = 0x24 + BTCHIP_INS_SET_OPERATION_MODE = 0x26 + BTCHIP_INS_SET_KEYMAP = 0x28 + BTCHIP_INS_SET_COMM_PROTOCOL = 0x2a + BTCHIP_INS_GET_WALLET_PUBLIC_KEY = 0x40 + BTCHIP_INS_GET_TRUSTED_INPUT = 0x42 + BTCHIP_INS_HASH_INPUT_START = 0x44 + BTCHIP_INS_HASH_INPUT_FINALIZE = 0x46 + BTCHIP_INS_HASH_SIGN = 0x48 + BTCHIP_INS_HASH_INPUT_FINALIZE_FULL = 0x4a + BTCHIP_INS_GET_INTERNAL_CHAIN_INDEX = 0x4c + BTCHIP_INS_SIGN_MESSAGE = 0x4e + BTCHIP_INS_GET_TRANSACTION_LIMIT = 0xa0 + BTCHIP_INS_SET_TRANSACTION_LIMIT = 0xa2 + BTCHIP_INS_IMPORT_PRIVATE_KEY = 0xb0 + BTCHIP_INS_GET_PUBLIC_KEY = 0xb2 + BTCHIP_INS_DERIVE_BIP32_KEY = 0xb4 + BTCHIP_INS_SIGNVERIFY_IMMEDIATE = 0xb6 + BTCHIP_INS_GET_RANDOM = 0xc0 + BTCHIP_INS_GET_ATTESTATION = 0xc2 + BTCHIP_INS_GET_FIRMWARE_VERSION = 0xc4 + BTCHIP_INS_COMPOSE_MOFN_ADDRESS = 0xc6 + BTCHIP_INS_GET_POS_SEED = 0xca + + BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY = 0x20 + BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY = 0x22 + BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY = 0x24 + BTCHIP_INS_EXT_CACHE_GET_FEATURES = 0x26 + + OPERATION_MODE_WALLET = 0x01 + OPERATION_MODE_RELAXED_WALLET = 0x02 + OPERATION_MODE_SERVER = 0x04 + OPERATION_MODE_DEVELOPER = 0x08 + + FEATURE_UNCOMPRESSED_KEYS = 0x01 + FEATURE_RFC6979 = 0x02 + FEATURE_FREE_SIGHASHTYPE = 0x04 + FEATURE_NO_2FA_P2SH = 0x08 + + QWERTY_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f313035")) + QWERTZ_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f313035")) + AZERTY_KEYMAP = bytearray(unhexlify("08000000010000200100007820c8ffc3feffff07000000002c38202030341e21222d352e102e3637271e1f202122232425263736362e37101f1405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f64302f2d351405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f643035")) + + def __init__(self, dongle): + self.dongle = dongle + self.needKeyCache = False + try: + firmware = self.getFirmwareVersion()['version'] + self.multiOutputSupported = tuple(map(int, (firmware.split(".")))) >= (1, 1, 4) + if self.multiOutputSupported: + self.scriptBlockLength = 50 + else: + self.scriptBlockLength = 255 + except Exception: + pass + try: + result = self.getJCExtendedFeatures() + self.needKeyCache = (result['proprietaryApi'] == False) + except Exception: + pass + + def setAlternateCoinVersion(self, versionRegular, versionP2SH): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_ALTERNATE_COIN_VERSION, 0x00, 0x00, 0x02, versionRegular, versionP2SH] + self.dongle.exchange(bytearray(apdu)) + + def verifyPin(self, pin): + if isinstance(pin, str): + pin = pin.encode('utf-8') + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x00, 0x00, len(pin) ] + apdu.extend(bytearray(pin)) + self.dongle.exchange(bytearray(apdu)) + + def getVerifyPinRemainingAttempts(self): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x80, 0x00, 0x01 ] + apdu.extend(bytearray(b'0')) + try: + self.dongle.exchange(bytearray(apdu)) + except BTChipException as e: + if ((e.sw & 0xfff0) == 0x63c0): + return e.sw - 0x63c0 + raise e + + def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False): + result = {} + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_WALLET_PUBLIC_KEY, 0x01 if showOnScreen else 0x00, 0x03 if cashAddr else 0x02 if segwitNative else 0x01 if segwit else 0x00, len(donglePath) ] + apdu.extend(donglePath) + response = self.dongle.exchange(bytearray(apdu)) + offset = 0 + result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] + offset = offset + 1 + response[offset] + result['address'] = str(response[offset + 1 : offset + 1 + response[offset]]) + offset = offset + 1 + response[offset] + result['chainCode'] = response[offset : offset + 32] + return result + + @classmethod + def getTrustedInput(self, cmd, transaction, index): + result = {} + # Header + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x00, 0x00 ] + params = bytearray.fromhex("%.8x" % (index)) + params.extend(transaction.version) + writeVarint(len(transaction.inputs), params) + apdu.append(len(params)) + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + # Each input + for trinput in transaction.inputs: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] + params = bytearray(trinput.prevOut) + writeVarint(len(trinput.script), params) + apdu.append(len(params)) + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + offset = 0 + while True: + blockLength = 251 + if ((offset + blockLength) < len(trinput.script)): + dataLength = blockLength + else: + dataLength = len(trinput.script) - offset + params = bytearray(trinput.script[offset : offset + dataLength]) + if ((offset + dataLength) == len(trinput.script)): + params.extend(trinput.sequence) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(params) ] + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + offset += dataLength + if (offset >= len(trinput.script)): + break + # Number of outputs + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] + params = [] + writeVarint(len(transaction.outputs), params) + apdu.append(len(params)) + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + # Each output + indexOutput = 0 + for troutput in transaction.outputs: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] + params = bytearray(troutput.amount) + writeVarint(len(troutput.script), params) + apdu.append(len(params)) + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + offset = 0 + while (offset < len(troutput.script)): + blockLength = 255 + if ((offset + blockLength) < len(troutput.script)): + dataLength = blockLength + else: + dataLength = len(troutput.script) - offset + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, dataLength ] + apdu.extend(troutput.script[offset : offset + dataLength]) + cmd.transport.exchange_raw(bytearray(apdu)) + offset += dataLength + # Locktime + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(transaction.lockTime) ] + apdu.extend(transaction.lockTime) + _, response = cmd.transport.exchange_raw(bytearray(apdu)) + result['trustedInput'] = True + result['value'] = response + return result + + @classmethod + def startUntrustedTransaction(self, cmd, newTransaction, inputIndex, outputList, redeemScript, version=0x01, cashAddr=False, continueSegwit=False): + # Start building a fake transaction with the passed inputs + segwit = False + if newTransaction: + for passedOutput in outputList: + if ('witness' in passedOutput) and passedOutput['witness']: + segwit = True + break + if newTransaction: + if segwit: + p2 = 0x03 if cashAddr else 0x02 + else: + p2 = 0x00 + else: + p2 = 0x10 if continueSegwit else 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x00, p2 ] + params = bytearray([version, 0x00, 0x00, 0x00]) + writeVarint(len(outputList), params) + apdu.append(len(params)) + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + # Loop for each input + currentIndex = 0 + for passedOutput in outputList: + if ('sequence' in passedOutput) and passedOutput['sequence']: + sequence = bytearray(unhexlify(passedOutput['sequence'])) + else: + sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) # default sequence + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00 ] + params = [] + script = bytearray(redeemScript) + if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: + params.append(0x01) + elif ('witness' in passedOutput) and passedOutput['witness']: + params.append(0x02) + else: + params.append(0x00) + if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: + params.append(len(passedOutput['value'])) + params.extend(passedOutput['value']) + if currentIndex != inputIndex: + script = bytearray() + writeVarint(len(script), params) + apdu.append(len(params)) + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + offset = 0 + while(offset < len(script)): + blockLength = 255 + if ((offset + blockLength) < len(script)): + dataLength = blockLength + else: + dataLength = len(script) - offset + params = script[offset : offset + dataLength] + if ((offset + dataLength) == len(script)): + params.extend(sequence) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(params) ] + apdu.extend(params) + cmd.transport.exchange_raw(bytearray(apdu)) + offset += blockLength + if len(script) == 0: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(sequence) ] + apdu.extend(sequence) + cmd.transport.exchange_raw(bytearray(apdu)) + currentIndex += 1 + + @classmethod + def finalizeInput(self, cmd, outputAddress, amount, fees, changePath, rawTx=None): + alternateEncoding = False + donglePath = parse_bip32_path(changePath) + result = {} + outputs = None + if rawTx is not None: + try: + fullTx = bitcoinTransaction(bytearray(rawTx)) + outputs = fullTx.serializeOutputs() + if len(donglePath) != 0: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, 0xFF, 0x00 ] + params = [] + params.extend(donglePath) + apdu.append(len(params)) + apdu.extend(params) + _, response = cmd.transport.exchange_raw(bytearray(apdu)) + offset = 0 + while (offset < len(outputs)): + blockLength = 50 + if ((offset + blockLength) < len(outputs)): + dataLength = blockLength + p1 = 0x00 + else: + dataLength = len(outputs) - offset + p1 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ + p1, 0x00, dataLength ] + apdu.extend(outputs[offset : offset + dataLength]) + _, response = cmd.transport.exchange_raw(bytearray(apdu)) + offset += dataLength + alternateEncoding = True + except Exception: + pass + if not alternateEncoding: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ] + params = [] + params.append(len(outputAddress)) + params.extend(bytearray(outputAddress)) + writeHexAmountBE(btc_to_satoshi(str(amount)), params) + writeHexAmountBE(btc_to_satoshi(str(fees)), params) + params.extend(donglePath) + apdu.append(len(params)) + apdu.extend(params) + _, response = cmd.transport.exchange_raw(bytearray(apdu)) + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1 + response[0] + 1:] + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + offset = offset + 1 + result['keycardData'] = response[offset : offset + keycardDataLength] + offset = offset + keycardDataLength + result['secureScreenData'] = response[offset:] + if result['confirmationType'] == 0x04: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] + if outputs == None: + result['outputData'] = response[1 : 1 + response[0]] + else: + result['outputData'] = outputs + return result + + def finalizeInputFull(self, outputData): + result = {} + offset = 0 + encryptedOutputData = b"" + while (offset < len(outputData)): + blockLength = self.scriptBlockLength + if ((offset + blockLength) < len(outputData)): + dataLength = blockLength + p1 = 0x00 + else: + dataLength = len(outputData) - offset + p1 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ + p1, 0x00, dataLength ] + apdu.extend(outputData[offset : offset + dataLength]) + _, response = cmd.transport.exchange_raw(bytearray(apdu)) + encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] + offset += dataLength + if len(response) > 1: + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + else: + # Support for old style API before 1.0.2 + result['confirmationNeeded'] = response[0] != 0x00 + result['confirmationType'] = response[0] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1 + response[0] + 1:] # legacy + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + offset = offset + 1 + result['keycardData'] = response[offset : offset + keycardDataLength] + offset = offset + keycardDataLength + result['secureScreenData'] = response[offset:] + result['encryptedOutputData'] = encryptedOutputData + if result['confirmationType'] == 0x04: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] + return result + + @classmethod + def untrustedHashSign(self, cmd, path, pin="", lockTime=0, sighashType=0x01): + if isinstance(pin, str): + pin = pin.encode('utf-8') + donglePath = parse_bip32_path(path) + + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_SIGN, 0x00, 0x00 ] + params = [] + params.extend(donglePath) + params.append(len(pin)) + params.extend(bytearray(pin)) + writeUint32BE(lockTime, params) + params.append(sighashType) + apdu.append(len(params)) + apdu.extend(params) + _,result = cmd.transport.exchange_raw(bytearray(apdu)) + result = bytearray(result) + result[0] = 0x30 + return result + + def signMessagePrepareV1(self, path, message): + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, 0x00 ] + params = [] + params.extend(donglePath) + params.append(len(message)) + params.extend(bytearray(message)) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + result['confirmationNeeded'] = response[0] != 0x00 + result['confirmationType'] = response[0] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1:] + if result['confirmationType'] == 0x03: + result['secureScreenData'] = response[1:] + return result + + def signMessagePrepareV2(self, path, message): + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + result = {} + offset = 0 + encryptedOutputData = b"" + while (offset < len(message)): + params = []; + if offset == 0: + params.extend(donglePath) + params.append((len(message) >> 8) & 0xff) + params.append(len(message) & 0xff) + p2 = 0x01 + else: + p2 = 0x80 + blockLength = 255 - len(params) + if ((offset + blockLength) < len(message)): + dataLength = blockLength + else: + dataLength = len(message) - offset + params.extend(bytearray(message[offset : offset + dataLength])) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, p2 ] + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] + offset += blockLength + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + result['secureScreenData'] = response[offset:] + result['encryptedOutputData'] = encryptedOutputData + + return result + + def signMessagePrepare(self, path, message): + try: + result = self.signMessagePrepareV2(path, message) + except BTChipException as e: + if (e.sw == 0x6b00): # Old firmware version, try older method + result = self.signMessagePrepareV1(path, message) + else: + raise + return result + + def signMessageSign(self, pin=""): + if isinstance(pin, str): + pin = pin.encode('utf-8') + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x80, 0x00 ] + params = [] + if pin is not None: + params.append(len(pin)) + params.extend(bytearray(pin)) + else: + params.append(0x00) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + return response + + def setup(self, operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH, userPin, wipePin, keymapEncoding, seed=None, developerKey=None): + if isinstance(userPin, str): + userPin = userPin.encode('utf-8') + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SETUP, 0x00, 0x00 ] + params = [ operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH ] + params.append(len(userPin)) + params.extend(bytearray(userPin)) + if wipePin is not None: + if isinstance(wipePin, str): + wipePin = wipePin.encode('utf-8') + params.append(len(wipePin)) + params.extend(bytearray(wipePin)) + else: + params.append(0x00) + if seed is not None: + if len(seed) < 32 or len(seed) > 64: + raise BTChipException("Invalid seed length") + params.append(len(seed)) + params.extend(seed) + else: + params.append(0x00) + if developerKey is not None: + params.append(len(developerKey)) + params.extend(developerKey) + else: + params.append(0x00) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + result['trustedInputKey'] = response[0:16] + result['developerKey'] = response[16:] + self.setKeymapEncoding(keymapEncoding) + try: + self.setTypingBehaviour(0xff, 0xff, 0xff, 0x10) + except BTChipException as e: + if (e.sw == 0x6700): # Old firmware version, command not supported + pass + else: + raise + return result + + def setKeymapEncoding(self, keymapEncoding): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x00, 0x00 ] + apdu.append(len(keymapEncoding)) + apdu.extend(keymapEncoding) + self.dongle.exchange(bytearray(apdu)) + + def setTypingBehaviour(self, unitDelayStart, delayStart, unitDelayKey, delayKey): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x01, 0x00 ] + params = [] + writeUint32BE(unitDelayStart, params) + writeUint32BE(delayStart, params) + writeUint32BE(unitDelayKey, params) + writeUint32BE(delayKey, params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + + def getOperationMode(self): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_OPERATION_MODE, 0x00, 0x00, 0x00] + response = self.dongle.exchange(bytearray(apdu)) + return response[0] + + def setOperationMode(self, operationMode): + if operationMode != btchip.OPERATION_MODE_WALLET \ + and operationMode != btchip.OPERATION_MODE_RELAXED_WALLET \ + and operationMode != btchip.OPERATION_MODE_SERVER \ + and operationMode != btchip.OPERATION_MODE_DEVELOPER: + raise BTChipException("Invalid operation mode") + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, 0x00, 0x00, 0x01, operationMode ] + self.dongle.exchange(bytearray(apdu)) + + @classmethod + def enableAlternate2fa(self, cmd, persistent): + if persistent: + p1 = 0x02 + else: + p1 = 0x01 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, p1, 0x00, 0x01, btchip.OPERATION_MODE_WALLET ] + cmd.transport.exchange_raw(bytearray(apdu)) + + def getFirmwareVersion(self): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_FIRMWARE_VERSION, 0x00, 0x00, 0x00 ] + try: + response = self.dongle.exchange(bytearray(apdu)) + except BTChipException as e: + if (e.sw == 0x6985): + response = [0x00, 0x00, 0x01, 0x04, 0x03 ] + pass + else: + raise + result['compressedKeys'] = (response[0] == 0x01) + result['version'] = "%d.%d.%d" % (response[2], response[3], response[4]) + result['specialVersion'] = response[1] + return result + + def getRandom(self, size): + if size > 255: + raise BTChipException("Invalid size") + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_RANDOM, 0x00, 0x00, size ] + return self.dongle.exchange(bytearray(apdu)) + + def getPOSSeedKey(self): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x01, 0x00, 0x00 ] + return self.dongle.exchange(bytearray(apdu)) + + def getPOSEncryptedSeed(self): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x02, 0x00, 0x00 ] + return self.dongle.exchange(bytearray(apdu)) + + def importPrivateKey(self, data, isSeed=False): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_IMPORT_PRIVATE_KEY, (0x02 if isSeed else 0x01), 0x00 ] + apdu.append(len(data)) + apdu.extend(data) + return self.dongle.exchange(bytearray(apdu)) + + def getPublicKey(self, encodedPrivateKey): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(encodedPrivateKey) + 1) + apdu.append(len(encodedPrivateKey)) + apdu.extend(encodedPrivateKey) + response = self.dongle.exchange(bytearray(apdu)) + offset = 1 + result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] + offset = offset + 1 + response[offset] + if response[0] == 0x02: + result['chainCode'] = response[offset : offset + 32] + offset = offset + 32 + result['depth'] = response[offset] + offset = offset + 1 + result['parentFingerprint'] = response[offset : offset + 4] + offset = offset + 4 + result['childNumber'] = response[offset : offset + 4] + return result + + def deriveBip32Key(self, encodedPrivateKey, path): + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + offset = 1 + currentEncodedPrivateKey = encodedPrivateKey + while (offset < len(donglePath)): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_DERIVE_BIP32_KEY, 0x00, 0x00 ] + apdu.append(len(currentEncodedPrivateKey) + 1 + 4) + apdu.append(len(currentEncodedPrivateKey)) + apdu.extend(currentEncodedPrivateKey) + apdu.extend(donglePath[offset : offset + 4]) + currentEncodedPrivateKey = self.dongle.exchange(bytearray(apdu)) + offset = offset + 4 + return currentEncodedPrivateKey + + def signImmediate(self, encodedPrivateKey, data, deterministic=True): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGNVERIFY_IMMEDIATE, 0x00, (0x80 if deterministic else 0x00) ] + apdu.append(len(encodedPrivateKey) + len(data) + 2); + apdu.append(len(encodedPrivateKey)) + apdu.extend(encodedPrivateKey) + apdu.append(len(data)) + apdu.extend(data) + return self.dongle.exchange(bytearray(apdu)) + +# Functions dedicated to the Java Card interface when no proprietary API is available + + def parse_bip32_path_internal(self, path): + if len(path) == 0: + return [] + result = [] + elements = path.split('/') + for pathElement in elements: + element = pathElement.split('\'') + if len(element) == 1: + result.append(int(element[0])) + else: + result.append(0x80000000 | int(element[0])) + return result + + def serialize_bip32_path_internal(self, path): + result = [] + for pathElement in path: + writeUint32BE(pathElement, result) + return bytearray([ len(path) ] + result) + + def resolvePublicKey(self, path): + expandedPath = self.serialize_bip32_path_internal(path) + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(expandedPath)) + apdu.extend(expandedPath) + result = self.dongle.exchange(bytearray(apdu)) + if (result[0] == 0): + # Not present, need to be inserted into the cache + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(expandedPath)) + apdu.extend(expandedPath) + result = self.dongle.exchange(bytearray(apdu)) + hashData = result[0:32] + keyX = result[32:64] + signature = result[64:] + keyXY = recoverKey(signature, hashData, keyX) + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(expandedPath) + 65) + apdu.extend(expandedPath) + apdu.extend(keyXY) + self.dongle.exchange(bytearray(apdu)) + + def resolvePublicKeysInPath(self, path): + splitPath = self.parse_bip32_path_internal(path) + # Locate the first public key in path + offset = 0 + startOffset = 0 + while(offset < len(splitPath)): + if (splitPath[offset] < 0x80000000): + startOffset = offset + break + offset = offset + 1 + if startOffset != 0: + searchPath = splitPath[0:startOffset - 1] + offset = startOffset - 1 + while(offset < len(splitPath)): + searchPath = searchPath + [ splitPath[offset] ] + self.resolvePublicKey(searchPath) + offset = offset + 1 + self.resolvePublicKey(splitPath) + + def getJCExtendedFeatures(self): + result = {} + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_GET_FEATURES, 0x00, 0x00, 0x00 ] + response = self.dongle.exchange(bytearray(apdu)) + result['proprietaryApi'] = ((response[0] & 0x01) != 0) + return result diff --git a/tests/btchippython/btchip/btchipComm.py b/tests/btchippython/btchip/btchipComm.py new file mode 100644 index 00000000..3a5fe001 --- /dev/null +++ b/tests/btchippython/btchip/btchipComm.py @@ -0,0 +1,254 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from abc import ABCMeta, abstractmethod +from .btchipException import * +from .ledgerWrapper import wrapCommandAPDU, unwrapResponseAPDU +from binascii import hexlify +import time +import os +import struct +import socket + +try: + import hid + HID = True +except ImportError: + HID = False + +try: + from smartcard.Exceptions import NoCardException + from smartcard.System import readers + from smartcard.util import toHexString, toBytes + SCARD = True +except ImportError: + SCARD = False + +class DongleWait(object): + __metaclass__ = ABCMeta + + @abstractmethod + def waitFirstResponse(self, timeout): + pass + +class Dongle(object): + __metaclass__ = ABCMeta + + @abstractmethod + def exchange(self, apdu, timeout=20000): + pass + + @abstractmethod + def close(self): + pass + + def setWaitImpl(self, waitImpl): + self.waitImpl = waitImpl + +class HIDDongleHIDAPI(Dongle, DongleWait): + + def __init__(self, device, ledger=False, debug=False): + self.device = device + self.ledger = ledger + self.debug = debug + self.waitImpl = self + self.opened = True + + def exchange(self, apdu, timeout=20000): + if self.debug: + print("=> %s" % hexlify(apdu)) + if self.ledger: + apdu = wrapCommandAPDU(0x0101, apdu, 64) + padSize = len(apdu) % 64 + tmp = apdu + if padSize != 0: + tmp.extend([0] * (64 - padSize)) + offset = 0 + while(offset != len(tmp)): + data = tmp[offset:offset + 64] + data = bytearray([0]) + data + self.device.write(data) + offset += 64 + dataLength = 0 + dataStart = 2 + result = self.waitImpl.waitFirstResponse(timeout) + if not self.ledger: + if result[0] == 0x61: # 61xx : data available + self.device.set_nonblocking(False) + dataLength = result[1] + dataLength += 2 + if dataLength > 62: + remaining = dataLength - 62 + while(remaining != 0): + if remaining > 64: + blockLength = 64 + else: + blockLength = remaining + result.extend(bytearray(self.device.read(65))[0:blockLength]) + remaining -= blockLength + swOffset = dataLength + dataLength -= 2 + self.device.set_nonblocking(True) + else: + swOffset = 0 + else: + self.device.set_nonblocking(False) + while True: + response = unwrapResponseAPDU(0x0101, result, 64) + if response is not None: + result = response + dataStart = 0 + swOffset = len(response) - 2 + dataLength = len(response) - 2 + self.device.set_nonblocking(True) + break + result.extend(bytearray(self.device.read(65))) + sw = (result[swOffset] << 8) + result[swOffset + 1] + response = result[dataStart : dataLength + dataStart] + if self.debug: + print("<= %s%.2x" % (hexlify(response), sw)) + if sw != 0x9000: + raise BTChipException("Invalid status %04x" % sw, sw) + return response + + def waitFirstResponse(self, timeout): + start = time.time() + data = "" + while len(data) == 0: + data = self.device.read(65) + if not len(data): + if time.time() - start > timeout: + raise BTChipException("Timeout") + time.sleep(0.02) + return bytearray(data) + + def close(self): + if self.opened: + try: + self.device.close() + except Exception: + pass + self.opened = False + +class DongleSmartcard(Dongle): + + def __init__(self, device, debug=False): + self.device = device + self.debug = debug + self.waitImpl = self + self.opened = True + + def exchange(self, apdu, timeout=20000): + if self.debug: + print("=> %s" % hexlify(apdu)) + response, sw1, sw2 = self.device.transmit(toBytes(hexlify(apdu))) + sw = (sw1 << 8) | sw2 + if self.debug: + print("<= %s%.2x" % (toHexString(response).replace(" ", ""), sw)) + if sw != 0x9000: + raise BTChipException("Invalid status %04x" % sw, sw) + return bytearray(response) + + def close(self): + if self.opened: + try: + self.device.disconnect() + except Exception: + pass + self.opened = False + +class DongleServer(Dongle): + + def __init__(self, server, port, debug=False): + self.server = server + self.port = port + self.debug = debug + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.socket.connect((self.server, self.port)) + except Exception: + raise BTChipException("Proxy connection failed") + + def exchange(self, apdu, timeout=20000): + if self.debug: + print("=> %s" % hexlify(apdu)) + self.socket.send(struct.pack(">I", len(apdu))) + self.socket.send(apdu) + size = struct.unpack(">I", self.socket.recv(4))[0] + response = self.socket.recv(size) + sw = struct.unpack(">H", self.socket.recv(2))[0] + if self.debug: + print("<= %s%.2x" % (hexlify(response), sw)) + if sw != 0x9000: + raise BTChipException("Invalid status %04x" % sw, sw) + return bytearray(response) + + def close(self): + try: + self.socket.close() + except Exception: + pass + +def getDongle(debug=False): + dev = None + hidDevicePath = None + ledger = False + if HID: + for hidDevice in hid.enumerate(0, 0): + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x2b7c: + hidDevicePath = hidDevice['path'] + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x3b7c: + hidDevicePath = hidDevice['path'] + ledger = True + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x4b7c: + hidDevicePath = hidDevice['path'] + ledger = True + if hidDevice['vendor_id'] == 0x2c97: + if ('interface_number' in hidDevice and hidDevice['interface_number'] == 0) or ('usage_page' in hidDevice and hidDevice['usage_page'] == 0xffa0): + hidDevicePath = hidDevice['path'] + ledger = True + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x1807: + hidDevicePath = hidDevice['path'] + if hidDevicePath is not None: + dev = hid.device() + dev.open_path(hidDevicePath) + dev.set_nonblocking(True) + return HIDDongleHIDAPI(dev, ledger, debug) + + if SCARD: + connection = None + for reader in readers(): + try: + connection = reader.createConnection() + connection.connect() + response, sw1, sw2 = connection.transmit(toBytes("00A4040010FF4C4547522E57414C5430312E493031")) + sw = (sw1 << 8) | sw2 + if sw == 0x9000: + break + else: + connection.disconnect() + connection = None + except Exception: + connection = None + pass + if connection is not None: + return DongleSmartcard(connection, debug) + if (os.getenv("LEDGER_PROXY_ADDRESS") is not None) and (os.getenv("LEDGER_PROXY_PORT") is not None): + return DongleServer(os.getenv("LEDGER_PROXY_ADDRESS"), int(os.getenv("LEDGER_PROXY_PORT")), debug) + raise BTChipException("No dongle found") diff --git a/tests/btchippython/btchip/btchipException.py b/tests/btchippython/btchip/btchipException.py new file mode 100644 index 00000000..ec577289 --- /dev/null +++ b/tests/btchippython/btchip/btchipException.py @@ -0,0 +1,28 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +class BTChipException(Exception): + + def __init__(self, message, sw=0x6f00): + self.message = message + self.sw = sw + + def __str__(self): + buf = "Exception : " + self.message + return buf diff --git a/tests/btchippython/btchip/btchipFirmwareWizard.py b/tests/btchippython/btchip/btchipFirmwareWizard.py new file mode 100644 index 00000000..06522110 --- /dev/null +++ b/tests/btchippython/btchip/btchipFirmwareWizard.py @@ -0,0 +1,24 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +def checkFirmware(version): + return True + +def updateFirmware(): + raise Exception("Unsupported BTChip firmware - please update your firmware from https://firmwareupdate.hardwarewallet.com") diff --git a/tests/btchippython/btchip/btchipHelpers.py b/tests/btchippython/btchip/btchipHelpers.py new file mode 100644 index 00000000..ba5b66c5 --- /dev/null +++ b/tests/btchippython/btchip/btchipHelpers.py @@ -0,0 +1,86 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +import decimal +import re + +# from pycoin +SATOSHI_PER_COIN = decimal.Decimal(1e8) +COIN_PER_SATOSHI = decimal.Decimal(1)/SATOSHI_PER_COIN + +def satoshi_to_btc(satoshi_count): + if satoshi_count == 0: + return decimal.Decimal(0) + r = satoshi_count * COIN_PER_SATOSHI + return r.normalize() + +def btc_to_satoshi(btc): + return int(decimal.Decimal(btc) * SATOSHI_PER_COIN) +# /from pycoin + +def writeUint32BE(value, buffer): + buffer.append((value >> 24) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append(value & 0xff) + return buffer + +def writeUint32LE(value, buffer): + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 24) & 0xff) + return buffer + +def writeHexAmount(value, buffer): + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 24) & 0xff) + buffer.append((value >> 32) & 0xff) + buffer.append((value >> 40) & 0xff) + buffer.append((value >> 48) & 0xff) + buffer.append((value >> 56) & 0xff) + return buffer + +def writeHexAmountBE(value, buffer): + buffer.append((value >> 56) & 0xff) + buffer.append((value >> 48) & 0xff) + buffer.append((value >> 40) & 0xff) + buffer.append((value >> 32) & 0xff) + buffer.append((value >> 24) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append(value & 0xff) + return buffer + +def parse_bip32_path(path): + if len(path) == 0: + return bytearray([ 0 ]) + result = [] + elements = path.split('/') + if len(elements) > 10: + raise BTChipException("Path too long") + for pathElement in elements: + element = re.split('\'|h|H', pathElement) + if len(element) == 1: + writeUint32BE(int(element[0]), result) + else: + writeUint32BE(0x80000000 | int(element[0]), result) + return bytearray([ len(elements) ] + result) diff --git a/tests/btchippython/btchip/btchipKeyRecovery.py b/tests/btchippython/btchip/btchipKeyRecovery.py new file mode 100644 index 00000000..20fa7705 --- /dev/null +++ b/tests/btchippython/btchip/btchipKeyRecovery.py @@ -0,0 +1,57 @@ +# From Electrum + +import ecdsa +from ecdsa.curves import SECP256k1 +from ecdsa.ellipticcurve import Point +from ecdsa.util import string_to_number, number_to_string + +class MyVerifyingKey(ecdsa.VerifyingKey): + @classmethod + def from_signature(klass, sig, recid, h, curve): + """ See http://www.secg.org/download/aid-780/sec1-v2.pdf, chapter 4.1.6 """ + from ecdsa import util, numbertheory + import msqr + curveFp = curve.curve + G = curve.generator + order = G.order() + # extract r,s from signature + r, s = util.sigdecode_string(sig, order) + # 1.1 + x = r + (recid/2) * order + # 1.3 + alpha = ( x * x * x + curveFp.a() * x + curveFp.b() ) % curveFp.p() + beta = msqr.modular_sqrt(alpha, curveFp.p()) + y = beta if (beta - recid) % 2 == 0 else curveFp.p() - beta + # 1.4 the constructor checks that nR is at infinity + R = Point(curveFp, x, y, order) + # 1.5 compute e from message: + e = string_to_number(h) + minus_e = -e % order + # 1.6 compute Q = r^-1 (sR - eG) + inv_r = numbertheory.inverse_mod(r,order) + Q = inv_r * ( s * R + minus_e * G ) + return klass.from_public_point( Q, curve ) + +def point_to_ser(P): + return ( '04'+('%064x'%P.x())+('%064x'%P.y()) ).decode('hex') + +def recoverKey(signature, hashValue, keyX): + rLength = signature[3] + r = signature[4 : 4 + rLength] + sLength = signature[4 + rLength + 1] + s = signature[4 + rLength + 2:] + if rLength == 33: + r = r[1:] + if sLength == 33: + s = s[1:] + r = str(r) + s = str(s) + for i in range(4): + try: + key = MyVerifyingKey.from_signature(r + s, i, hashValue, curve = SECP256k1) + candidate = point_to_ser(key.pubkey.point) + if candidate[1:33] == keyX: + return candidate + except Exception: + pass + raise Exception("Key recovery failed") diff --git a/tests/btchippython/btchip/btchipPersoWizard.py b/tests/btchippython/btchip/btchipPersoWizard.py new file mode 100644 index 00000000..a157410d --- /dev/null +++ b/tests/btchippython/btchip/btchipPersoWizard.py @@ -0,0 +1,376 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +import sys + +from PyQt4 import QtCore, QtGui +from PyQt4.QtGui import QDialog, QMessageBox + +try: + from mnemonic import Mnemonic + MNEMONIC = True +except Exception: + MNEMONIC = False + +from .btchipComm import getDongle, DongleWait +from .btchip import btchip +from .btchipUtils import compress_public_key,format_transaction, get_regular_input_script +from .bitcoinTransaction import bitcoinTransaction +from .btchipException import BTChipException + +import ui.personalization00start +import ui.personalization01seed +import ui.personalization02security +import ui.personalization03config +import ui.personalization04finalize +import ui.personalizationseedbackup01 +import ui.personalizationseedbackup02 +import ui.personalizationseedbackup03 +import ui.personalizationseedbackup04 + +BTCHIP_DEBUG = False + +def waitDongle(currentDialog, persoData): + try: + if persoData['client'] != None: + try: + persoData['client'].dongle.close() + except Exception: + pass + dongle = getDongle(BTCHIP_DEBUG) + persoData['client'] = btchip(dongle) + persoData['client'].getFirmwareVersion()['version'].split(".") + return True + except BTChipException as e: + if e.sw == 0x6faa: + QMessageBox.information(currentDialog, "BTChip Setup", "Please unplug the dongle and plug it again", "OK") + return False + if QMessageBox.question(currentDialog, "BTChip setup", "BTChip dongle not found. It might be in the wrong mode. Try unplugging und plugging it back in again, then press 'OK'", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes: + return False + else: + raise Exception("Aborted by user") + except Exception as e: + if QMessageBox.question(currentDialog, "BTChip setup", "BTChip dongle not found. It might be in the wrong mode. Try unplugging und plugging it back in again, then press 'OK'", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes: + return False + else: + raise Exception("Aborted by user") + + +class StartBTChipPersoDialog(QtGui.QDialog): + + def __init__(self): + QDialog.__init__(self, None) + self.ui = ui.personalization00start.Ui_Dialog() + self.ui.setupUi(self) + self.ui.NextButton.clicked.connect(self.processNext) + self.ui.CancelButton.clicked.connect(self.processCancel) + + def processNext(self): + persoData = {} + persoData['currencyCode'] = 0x00 + persoData['currencyCodeP2SH'] = 0x05 + persoData['client'] = None + dialog = SeedDialog(persoData, self) + persoData['main'] = self + dialog.exec_() + pass + + def processCancel(self): + self.reject() + +class SeedDialog(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalization01seed.Ui_Dialog() + self.ui.setupUi(self) + self.ui.seed.setEnabled(False) + self.ui.RestoreWalletButton.toggled.connect(self.restoreWalletToggled) + self.ui.NextButton.clicked.connect(self.processNext) + self.ui.CancelButton.clicked.connect(self.processCancel) + if MNEMONIC: + self.mnemonic = Mnemonic('english') + self.ui.mnemonicNotAvailableLabel.hide() + + def restoreWalletToggled(self, toggled): + self.ui.seed.setEnabled(toggled) + + def processNext(self): + self.persoData['seed'] = None + if self.ui.RestoreWalletButton.isChecked(): + # Check if it's an hexa string + seedText = str(self.ui.seed.text()) + if len(seedText) == 0: + QMessageBox.warning(self, "Error", "Please enter a seed", "OK") + return + if seedText[-1] == 'X': + seedText = seedText[0:-1] + try: + self.persoData['seed'] = seedText.decode('hex') + except Exception: + pass + if self.persoData['seed'] == None: + if not MNEMONIC: + QMessageBox.warning(self, "Error", "Mnemonic API not available. Please install https://github.com/trezor/python-mnemonic", "OK") + return + if not self.mnemonic.check(seedText): + QMessageBox.warning(self, "Error", "Invalid mnemonic", "OK") + return + self.persoData['seed'] = Mnemonic.to_seed(seedText) + else: + if (len(self.persoData['seed']) < 32) or (len(self.persoData['seed']) > 64): + QMessageBox.warning(self, "Error", "Invalid seed length", "OK") + return + dialog = SecurityDialog(self.persoData, self) + self.hide() + dialog.exec_() + + def processCancel(self): + self.reject() + self.persoData['main'].reject() + +class SecurityDialog(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalization02security.Ui_Dialog() + self.ui.setupUi(self) + self.ui.NextButton.clicked.connect(self.processNext) + self.ui.CancelButton.clicked.connect(self.processCancel) + + + def processNext(self): + if (self.ui.pin1.text() != self.ui.pin2.text()): + self.ui.pin1.setText("") + self.ui.pin2.setText("") + QMessageBox.warning(self, "Error", "PINs are not matching", "OK") + return + if (len(self.ui.pin1.text()) < 4): + QMessageBox.warning(self, "Error", "PIN must be at least 4 characteres long", "OK") + return + if (len(self.ui.pin1.text()) > 32): + QMessageBox.warning(self, "Error", "PIN is too long", "OK") + return + self.persoData['pin'] = str(self.ui.pin1.text()) + self.persoData['hardened'] = self.ui.HardenedButton.isChecked() + dialog = ConfigDialog(self.persoData, self) + self.hide() + dialog.exec_() + + def processCancel(self): + self.reject() + self.persoData['main'].reject() + +class ConfigDialog(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalization03config.Ui_Dialog() + self.ui.setupUi(self) + self.ui.NextButton.clicked.connect(self.processNext) + self.ui.CancelButton.clicked.connect(self.processCancel) + + def processNext(self): + if (self.ui.qwertyButton.isChecked()): + self.persoData['keyboard'] = btchip.QWERTY_KEYMAP + elif (self.ui.qwertzButton.isChecked()): + self.persoData['keyboard'] = btchip.QWERTZ_KEYMAP + elif (self.ui.azertyButton.isChecked()): + self.persoData['keyboard'] = btchip.AZERTY_KEYMAP + try: + while not waitDongle(self, self.persoData): + pass + except Exception as e: + self.reject() + self.persoData['main'].reject() + mode = btchip.OPERATION_MODE_WALLET + if not self.persoData['hardened']: + mode = mode | btchip.OPERATION_MODE_SERVER + try: + self.persoData['client'].setup(mode, btchip.FEATURE_RFC6979, self.persoData['currencyCode'], + self.persoData['currencyCodeP2SH'], self.persoData['pin'], None, + self.persoData['keyboard'], self.persoData['seed']) + except BTChipException as e: + if e.sw == 0x6985: + QMessageBox.warning(self, "Error", "Dongle is already set up. Please insert a different one", "OK") + return + except Exception as e: + QMessageBox.warning(self, "Error", "Error performing setup", "OK") + return + if self.persoData['seed'] is None: + dialog = SeedBackupStart(self.persoData, self) + self.hide() + dialog.exec_() + else: + dialog = FinalizeDialog(self.persoData, self) + self.hide() + dialog.exec_() + + def processCancel(self): + self.reject() + self.persoData['main'].reject() + +class FinalizeDialog(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalization04finalize.Ui_Dialog() + self.ui.setupUi(self) + self.ui.FinishButton.clicked.connect(self.finish) + try: + while not waitDongle(self, self.persoData): + pass + except Exception as e: + self.reject() + self.persoData['main'].reject() + attempts = self.persoData['client'].getVerifyPinRemainingAttempts() + self.ui.remainingAttemptsLabel.setText("Remaining attempts " + str(attempts)) + + def finish(self): + if (len(self.ui.pin1.text()) < 4): + QMessageBox.warning(self, "Error", "PIN must be at least 4 characteres long", "OK") + return + if (len(self.ui.pin1.text()) > 32): + QMessageBox.warning(self, "Error", "PIN is too long", "OK") + return + try: + self.persoData['client'].verifyPin(str(self.ui.pin1.text())) + except BTChipException as e: + if ((e.sw == 0x63c0) or (e.sw == 0x6985)): + QMessageBox.warning(self, "Error", "Invalid PIN - dongle has been reset. Please personalize again", "OK") + self.reject() + self.persoData['main'].reject() + if ((e.sw & 0xfff0) == 0x63c0): + attempts = e.sw - 0x63c0 + self.ui.remainingAttemptsLabel.setText("Remaining attempts " + str(attempts)) + QMessageBox.warning(self, "Error", "Invalid PIN - please unplug the dongle and plug it again before retrying", "OK") + try: + while not waitDongle(self, self.persoData): + pass + except Exception as e: + self.reject() + self.persoData['main'].reject() + return + except Exception as e: + QMessageBox.warning(self, "Error", "Unexpected error verifying PIN - aborting", "OK") + self.reject() + self.persoData['main'].reject() + return + if not self.persoData['hardened']: + try: + self.persoData['client'].setOperationMode(btchip.OPERATION_MODE_SERVER) + except Exception: + QMessageBox.warning(self, "Error", "Error switching to non hardened mode", "OK") + self.reject() + self.persoData['main'].reject() + return + QMessageBox.information(self, "BTChip Setup", "Setup completed. Please unplug the dongle and plug it again before use", "OK") + self.accept() + self.persoData['main'].accept() + +class SeedBackupStart(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalizationseedbackup01.Ui_Dialog() + self.ui.setupUi(self) + self.ui.NextButton.clicked.connect(self.processNext) + + def processNext(self): + dialog = SeedBackupUnplug(self.persoData, self) + self.hide() + dialog.exec_() + +class SeedBackupUnplug(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalizationseedbackup02.Ui_Dialog() + self.ui.setupUi(self) + self.ui.NextButton.clicked.connect(self.processNext) + + def processNext(self): + dialog = SeedBackupInstructions(self.persoData, self) + self.hide() + dialog.exec_() + +class SeedBackupInstructions(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalizationseedbackup03.Ui_Dialog() + self.ui.setupUi(self) + self.ui.NextButton.clicked.connect(self.processNext) + + def processNext(self): + dialog = SeedBackupVerify(self.persoData, self) + self.hide() + dialog.exec_() + +class SeedBackupVerify(QtGui.QDialog): + + def __init__(self, persoData, parent = None): + QDialog.__init__(self, parent) + self.persoData = persoData + self.ui = ui.personalizationseedbackup04.Ui_Dialog() + self.ui.setupUi(self) + self.ui.seedOkButton.clicked.connect(self.seedOK) + self.ui.seedKoButton.clicked.connect(self.seedKO) + + def seedOK(self): + dialog = FinalizeDialog(self.persoData, self) + self.hide() + dialog.exec_() + + def seedKO(self): + finished = False + while not finished: + try: + while not waitDongle(self, self.persoData): + pass + except Exception as e: + pass + try: + self.persoData['client'].verifyPin("0") + except BTChipException as e: + if e.sw == 0x63c0: + QMessageBox.information(self, "BTChip Setup", "Dongle is reset and can be repersonalized", "OK") + finished = True + pass + if e.sw == 0x6faa: + QMessageBox.information(self, "BTChip Setup", "Please unplug the dongle and plug it again", "OK") + pass + except Exception as e: + pass + self.reject() + self.persoData['main'].reject() + +if __name__ == "__main__": + + app = QtGui.QApplication(sys.argv) + dialog = StartBTChipPersoDialog() + dialog.show() + app.exec_() diff --git a/tests/btchippython/btchip/btchipUtils.py b/tests/btchippython/btchip/btchipUtils.py new file mode 100644 index 00000000..0bf2fa2a --- /dev/null +++ b/tests/btchippython/btchip/btchipUtils.py @@ -0,0 +1,105 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .btchipException import * +from .bitcoinTransaction import * +from .btchipHelpers import * + +def compress_public_key(publicKey): + if publicKey[0] == 0x04: + if (publicKey[64] & 1) != 0: + prefix = 0x03 + else: + prefix = 0x02 + result = [prefix] + result.extend(publicKey[1:33]) + return bytearray(result) + elif publicKey[0] == 0x03 or publicKey[0] == 0x02: + return publicKey + else: + raise BTChipException("Invalid public key format") + +def format_transaction(dongleOutputData, trustedInputsAndInputScripts, version=0x01, lockTime=0): + transaction = bitcoinTransaction() + transaction.version = [] + writeUint32LE(version, transaction.version) + for item in trustedInputsAndInputScripts: + newInput = bitcoinInput() + newInput.prevOut = item[0][4:4+36] + newInput.script = item[1] + if len(item) > 2: + newInput.sequence = bytearray(item[2].decode('hex')) + else: + newInput.sequence = bytearray([0xff, 0xff, 0xff, 0xff]) + transaction.inputs.append(newInput) + result = transaction.serialize(True) + result.extend(dongleOutputData) + writeUint32LE(lockTime, result) + return bytearray(result) + +def get_regular_input_script(sigHashtype, publicKey): + if len(sigHashtype) >= 0x4c: + raise BTChipException("Invalid sigHashtype") + if len(publicKey) >= 0x4c: + raise BTChipException("Invalid publicKey") + result = [ len(sigHashtype) ] + result.extend(sigHashtype) + result.append(len(publicKey)) + result.extend(publicKey) + return bytearray(result) + +def write_pushed_data_size(data, buffer): + if (len(data) > 0xffff): + raise BTChipException("unsupported encoding") + if (len(data) < 0x4c): + buffer.append(len(data)) + elif (len(data) > 255): + buffer.append(0x4d) + buffer.append(len(data) & 0xff) + buffer.append((len(data) >> 8) & 0xff) + else: + buffer.append(0x4c) + buffer.append(len(data)) + return buffer + + +def get_p2sh_input_script(redeemScript, sigHashtypeList): + result = [ 0x00 ] + for sigHashtype in sigHashtypeList: + write_pushed_data_size(sigHashtype, result) + result.extend(sigHashtype) + write_pushed_data_size(redeemScript, result) + result.extend(redeemScript) + return bytearray(result) + +def get_p2pk_input_script(sigHashtype): + if len(sigHashtype) >= 0x4c: + raise BTChipException("Invalid sigHashtype") + result = [ len(sigHashtype) ] + result.extend(sigHashtype) + return bytearray(result) + +def get_output_script(amountScriptArray): + result = [ len(amountScriptArray) ] + for amountScript in amountScriptArray: + writeHexAmount(btc_to_satoshi(str(amountScript[0])), result) + writeVarint(len(amountScript[1]), result) + result.extend(amountScript[1]) + return bytearray(result) + diff --git a/tests/btchippython/btchip/ledgerWrapper.py b/tests/btchippython/btchip/ledgerWrapper.py new file mode 100644 index 00000000..44fde1db --- /dev/null +++ b/tests/btchippython/btchip/ledgerWrapper.py @@ -0,0 +1,92 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +import struct +from .btchipException import BTChipException + +def wrapCommandAPDU(channel, command, packetSize): + if packetSize < 3: + raise BTChipException("Can't handle Ledger framing with less than 3 bytes for the report") + sequenceIdx = 0 + offset = 0 + result = struct.pack(">HBHH", channel, 0x05, sequenceIdx, len(command)) + sequenceIdx = sequenceIdx + 1 + if len(command) > packetSize - 7: + blockSize = packetSize - 7 + else: + blockSize = len(command) + result += command[offset : offset + blockSize] + offset = offset + blockSize + while offset != len(command): + result += struct.pack(">HBH", channel, 0x05, sequenceIdx) + sequenceIdx = sequenceIdx + 1 + if (len(command) - offset) > packetSize - 5: + blockSize = packetSize - 5 + else: + blockSize = len(command) - offset + result += command[offset : offset + blockSize] + offset = offset + blockSize + while (len(result) % packetSize) != 0: + result += b"\x00" + return bytearray(result) + +def unwrapResponseAPDU(channel, data, packetSize): + sequenceIdx = 0 + offset = 0 + if ((data is None) or (len(data) < 7 + 5)): + return None + if struct.unpack(">H", data[offset : offset + 2])[0] != channel: + raise BTChipException("Invalid channel") + offset += 2 + if data[offset] != 0x05: + raise BTChipException("Invalid tag") + offset += 1 + if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: + raise BTChipException("Invalid sequence") + offset += 2 + responseLength = struct.unpack(">H", data[offset : offset + 2])[0] + offset += 2 + if len(data) < 7 + responseLength: + return None + if responseLength > packetSize - 7: + blockSize = packetSize - 7 + else: + blockSize = responseLength + result = data[offset : offset + blockSize] + offset += blockSize + while (len(result) != responseLength): + sequenceIdx = sequenceIdx + 1 + if (offset == len(data)): + return None + if struct.unpack(">H", data[offset : offset + 2])[0] != channel: + raise BTChipException("Invalid channel") + offset += 2 + if data[offset] != 0x05: + raise BTChipException("Invalid tag") + offset += 1 + if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: + raise BTChipException("Invalid sequence") + offset += 2 + if (responseLength - len(result)) > packetSize - 5: + blockSize = packetSize - 5 + else: + blockSize = responseLength - len(result) + result += data[offset : offset + blockSize] + offset += blockSize + return bytearray(result) diff --git a/tests/btchippython/btchip/msqr.py b/tests/btchippython/btchip/msqr.py new file mode 100644 index 00000000..f582ec26 --- /dev/null +++ b/tests/btchippython/btchip/msqr.py @@ -0,0 +1,94 @@ +# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/ + +def modular_sqrt(a, p): + """ Find a quadratic residue (mod p) of 'a'. p + must be an odd prime. + + Solve the congruence of the form: + x^2 = a (mod p) + And returns x. Note that p - x is also a root. + + 0 is returned is no square root exists for + these a and p. + + The Tonelli-Shanks algorithm is used (except + for some simple cases in which the solution + is known from an identity). This algorithm + runs in polynomial time (unless the + generalized Riemann hypothesis is false). + """ + # Simple cases + # + if legendre_symbol(a, p) != 1: + return 0 + elif a == 0: + return 0 + elif p == 2: + return p + elif p % 4 == 3: + return pow(a, (p + 1) / 4, p) + + # Partition p-1 to s * 2^e for an odd s (i.e. + # reduce all the powers of 2 from p-1) + # + s = p - 1 + e = 0 + while s % 2 == 0: + s /= 2 + e += 1 + + # Find some 'n' with a legendre symbol n|p = -1. + # Shouldn't take long. + # + n = 2 + while legendre_symbol(n, p) != -1: + n += 1 + + # Here be dragons! + # Read the paper "Square roots from 1; 24, 51, + # 10 to Dan Shanks" by Ezra Brown for more + # information + # + + # x is a guess of the square root that gets better + # with each iteration. + # b is the "fudge factor" - by how much we're off + # with the guess. The invariant x^2 = ab (mod p) + # is maintained throughout the loop. + # g is used for successive powers of n to update + # both a and b + # r is the exponent - decreases with each update + # + x = pow(a, (s + 1) / 2, p) + b = pow(a, s, p) + g = pow(n, s, p) + r = e + + while True: + t = b + m = 0 + for m in xrange(r): + if t == 1: + break + t = pow(t, 2, p) + + if m == 0: + return x + + gs = pow(g, 2 ** (r - m - 1), p) + g = (gs * gs) % p + x = (x * gs) % p + b = (b * g) % p + r = m + +def legendre_symbol(a, p): + """ Compute the Legendre symbol a|p using + Euler's criterion. p is a prime, a is + relatively prime to p (if p divides + a, then a|p = 0) + + Returns 1 if a has a square root modulo + p, -1 otherwise. + """ + ls = pow(a, (p - 1) / 2, p) + return -1 if ls == p - 1 else ls diff --git a/tests/btchippython/btchip/ui/__init__.py b/tests/btchippython/btchip/ui/__init__.py new file mode 100644 index 00000000..9b5894e7 --- /dev/null +++ b/tests/btchippython/btchip/ui/__init__.py @@ -0,0 +1,19 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + diff --git a/tests/btchippython/btchip/ui/personalization00start.py b/tests/btchippython/btchip/ui/personalization00start.py new file mode 100644 index 00000000..0429bee5 --- /dev/null +++ b/tests/btchippython/btchip/ui/personalization00start.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-00-start.ui' +# +# Created: Fri Sep 19 15:03:25 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 231) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(120, 20, 231, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(20, 60, 351, 61)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.NextButton = QtGui.QPushButton(Dialog) + self.NextButton.setGeometry(QtCore.QRect(310, 200, 75, 25)) + self.NextButton.setObjectName(_fromUtf8("NextButton")) + self.arningLabel = QtGui.QLabel(Dialog) + self.arningLabel.setGeometry(QtCore.QRect(20, 120, 351, 81)) + self.arningLabel.setWordWrap(True) + self.arningLabel.setObjectName(_fromUtf8("arningLabel")) + self.CancelButton = QtGui.QPushButton(Dialog) + self.CancelButton.setGeometry(QtCore.QRect(20, 200, 75, 25)) + self.CancelButton.setObjectName(_fromUtf8("CancelButton")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Your BTChip dongle is not set up - you\'ll be able to create a new wallet, or restore an existing one, and choose your security profile.", None, QtGui.QApplication.UnicodeUTF8)) + self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) + self.arningLabel.setText(QtGui.QApplication.translate("Dialog", "Sensitive information including your dongle PIN will be exchanged during this setup phase - it is recommended to execute it on a secure computer, disconnected from any network, especially if you restore a wallet backup.", None, QtGui.QApplication.UnicodeUTF8)) + self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalization01seed.py b/tests/btchippython/btchip/ui/personalization01seed.py new file mode 100644 index 00000000..60fa2aee --- /dev/null +++ b/tests/btchippython/btchip/ui/personalization01seed.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-01-seed.ui' +# +# Created: Thu Aug 28 22:26:22 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 300) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(50, 20, 311, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(20, 60, 351, 61)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.NewWalletButton = QtGui.QRadioButton(Dialog) + self.NewWalletButton.setGeometry(QtCore.QRect(20, 130, 94, 21)) + self.NewWalletButton.setChecked(True) + self.NewWalletButton.setObjectName(_fromUtf8("NewWalletButton")) + self.buttonGroup = QtGui.QButtonGroup(Dialog) + self.buttonGroup.setObjectName(_fromUtf8("buttonGroup")) + self.buttonGroup.addButton(self.NewWalletButton) + self.RestoreWalletButton = QtGui.QRadioButton(Dialog) + self.RestoreWalletButton.setGeometry(QtCore.QRect(20, 180, 171, 21)) + self.RestoreWalletButton.setObjectName(_fromUtf8("RestoreWalletButton")) + self.buttonGroup.addButton(self.RestoreWalletButton) + self.seed = QtGui.QLineEdit(Dialog) + self.seed.setEnabled(False) + self.seed.setGeometry(QtCore.QRect(50, 210, 331, 21)) + self.seed.setEchoMode(QtGui.QLineEdit.Normal) + self.seed.setObjectName(_fromUtf8("seed")) + self.CancelButton = QtGui.QPushButton(Dialog) + self.CancelButton.setGeometry(QtCore.QRect(10, 270, 75, 25)) + self.CancelButton.setObjectName(_fromUtf8("CancelButton")) + self.NextButton = QtGui.QPushButton(Dialog) + self.NextButton.setGeometry(QtCore.QRect(320, 270, 75, 25)) + self.NextButton.setObjectName(_fromUtf8("NextButton")) + self.mnemonicNotAvailableLabel = QtGui.QLabel(Dialog) + self.mnemonicNotAvailableLabel.setGeometry(QtCore.QRect(130, 240, 171, 31)) + font = QtGui.QFont() + font.setItalic(True) + self.mnemonicNotAvailableLabel.setFont(font) + self.mnemonicNotAvailableLabel.setWordWrap(True) + self.mnemonicNotAvailableLabel.setObjectName(_fromUtf8("mnemonicNotAvailableLabel")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup - seed", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed (1/3)", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please select an option : either create a new wallet or restore an existing one", None, QtGui.QApplication.UnicodeUTF8)) + self.NewWalletButton.setText(QtGui.QApplication.translate("Dialog", "New Wallet", None, QtGui.QApplication.UnicodeUTF8)) + self.RestoreWalletButton.setText(QtGui.QApplication.translate("Dialog", "Restore wallet backup", None, QtGui.QApplication.UnicodeUTF8)) + self.seed.setPlaceholderText(QtGui.QApplication.translate("Dialog", "Enter an hexadecimal seed or a BIP 39 mnemonic code", None, QtGui.QApplication.UnicodeUTF8)) + self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) + self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) + self.mnemonicNotAvailableLabel.setText(QtGui.QApplication.translate("Dialog", "Mnemonic API is not available", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalization02security.py b/tests/btchippython/btchip/ui/personalization02security.py new file mode 100644 index 00000000..f65c2896 --- /dev/null +++ b/tests/btchippython/btchip/ui/personalization02security.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-02-security.ui' +# +# Created: Thu Aug 28 22:26:22 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 503) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(20, 20, 361, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(10, 60, 351, 61)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.HardenedButton = QtGui.QRadioButton(Dialog) + self.HardenedButton.setGeometry(QtCore.QRect(20, 110, 81, 21)) + self.HardenedButton.setChecked(True) + self.HardenedButton.setObjectName(_fromUtf8("HardenedButton")) + self.buttonGroup = QtGui.QButtonGroup(Dialog) + self.buttonGroup.setObjectName(_fromUtf8("buttonGroup")) + self.buttonGroup.addButton(self.HardenedButton) + self.HardenedButton_2 = QtGui.QRadioButton(Dialog) + self.HardenedButton_2.setGeometry(QtCore.QRect(20, 210, 81, 21)) + self.HardenedButton_2.setObjectName(_fromUtf8("HardenedButton_2")) + self.buttonGroup.addButton(self.HardenedButton_2) + self.IntroLabel_2 = QtGui.QLabel(Dialog) + self.IntroLabel_2.setGeometry(QtCore.QRect(50, 140, 351, 61)) + self.IntroLabel_2.setWordWrap(True) + self.IntroLabel_2.setObjectName(_fromUtf8("IntroLabel_2")) + self.IntroLabel_3 = QtGui.QLabel(Dialog) + self.IntroLabel_3.setGeometry(QtCore.QRect(50, 230, 351, 61)) + self.IntroLabel_3.setWordWrap(True) + self.IntroLabel_3.setObjectName(_fromUtf8("IntroLabel_3")) + self.CancelButton = QtGui.QPushButton(Dialog) + self.CancelButton.setGeometry(QtCore.QRect(10, 470, 75, 25)) + self.CancelButton.setObjectName(_fromUtf8("CancelButton")) + self.NextButton = QtGui.QPushButton(Dialog) + self.NextButton.setGeometry(QtCore.QRect(310, 470, 75, 25)) + self.NextButton.setObjectName(_fromUtf8("NextButton")) + self.IntroLabel_4 = QtGui.QLabel(Dialog) + self.IntroLabel_4.setGeometry(QtCore.QRect(10, 300, 351, 61)) + self.IntroLabel_4.setWordWrap(True) + self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) + self.IntroLabel_5 = QtGui.QLabel(Dialog) + self.IntroLabel_5.setGeometry(QtCore.QRect(20, 380, 161, 31)) + self.IntroLabel_5.setWordWrap(True) + self.IntroLabel_5.setObjectName(_fromUtf8("IntroLabel_5")) + self.pin1 = QtGui.QLineEdit(Dialog) + self.pin1.setGeometry(QtCore.QRect(210, 380, 161, 21)) + self.pin1.setEchoMode(QtGui.QLineEdit.Password) + self.pin1.setObjectName(_fromUtf8("pin1")) + self.pin2 = QtGui.QLineEdit(Dialog) + self.pin2.setGeometry(QtCore.QRect(210, 420, 161, 21)) + self.pin2.setEchoMode(QtGui.QLineEdit.Password) + self.pin2.setObjectName(_fromUtf8("pin2")) + self.IntroLabel_6 = QtGui.QLabel(Dialog) + self.IntroLabel_6.setGeometry(QtCore.QRect(20, 420, 171, 31)) + self.IntroLabel_6.setWordWrap(True) + self.IntroLabel_6.setObjectName(_fromUtf8("IntroLabel_6")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup - security", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - security (2/3)", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please choose a security profile", None, QtGui.QApplication.UnicodeUTF8)) + self.HardenedButton.setText(QtGui.QApplication.translate("Dialog", "Hardened", None, QtGui.QApplication.UnicodeUTF8)) + self.HardenedButton_2.setText(QtGui.QApplication.translate("Dialog", "PIN only", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_2.setText(QtGui.QApplication.translate("Dialog", "You need to remove the dongle and insert it again to get a second factor validation of all operations. Recommended for expert users and to be fully protected against malwares.", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_3.setText(QtGui.QApplication.translate("Dialog", "You only need to enter a PIN once when inserting the dongle. Transactions are not protected against malwares", None, QtGui.QApplication.UnicodeUTF8)) + self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) + self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "Please choose a PIN associated to the BTChip dongle. The PIN protects the dongle in case it is stolen, and can be up to 32 characters. The dongle is wiped if a wrong PIN is entered 3 times in a row.", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_5.setText(QtGui.QApplication.translate("Dialog", "Enter the new PIN : ", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_6.setText(QtGui.QApplication.translate("Dialog", "Repeat the new PIN :", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalization03config.py b/tests/btchippython/btchip/ui/personalization03config.py new file mode 100644 index 00000000..02d96e5e --- /dev/null +++ b/tests/btchippython/btchip/ui/personalization03config.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-03-config.ui' +# +# Created: Thu Aug 28 22:26:22 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 243) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(30, 10, 361, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(20, 50, 351, 61)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.qwertyButton = QtGui.QRadioButton(Dialog) + self.qwertyButton.setGeometry(QtCore.QRect(50, 110, 94, 21)) + self.qwertyButton.setChecked(True) + self.qwertyButton.setObjectName(_fromUtf8("qwertyButton")) + self.keyboardGroup = QtGui.QButtonGroup(Dialog) + self.keyboardGroup.setObjectName(_fromUtf8("keyboardGroup")) + self.keyboardGroup.addButton(self.qwertyButton) + self.qwertzButton = QtGui.QRadioButton(Dialog) + self.qwertzButton.setGeometry(QtCore.QRect(50, 140, 94, 21)) + self.qwertzButton.setObjectName(_fromUtf8("qwertzButton")) + self.keyboardGroup.addButton(self.qwertzButton) + self.azertyButton = QtGui.QRadioButton(Dialog) + self.azertyButton.setGeometry(QtCore.QRect(50, 170, 94, 21)) + self.azertyButton.setObjectName(_fromUtf8("azertyButton")) + self.keyboardGroup.addButton(self.azertyButton) + self.CancelButton = QtGui.QPushButton(Dialog) + self.CancelButton.setGeometry(QtCore.QRect(10, 210, 75, 25)) + self.CancelButton.setObjectName(_fromUtf8("CancelButton")) + self.NextButton = QtGui.QPushButton(Dialog) + self.NextButton.setGeometry(QtCore.QRect(320, 210, 75, 25)) + self.NextButton.setObjectName(_fromUtf8("NextButton")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - config (3/3)", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please select your keyboard type to type the second factor confirmation", None, QtGui.QApplication.UnicodeUTF8)) + self.qwertyButton.setText(QtGui.QApplication.translate("Dialog", "QWERTY", None, QtGui.QApplication.UnicodeUTF8)) + self.qwertzButton.setText(QtGui.QApplication.translate("Dialog", "QWERTZ", None, QtGui.QApplication.UnicodeUTF8)) + self.azertyButton.setText(QtGui.QApplication.translate("Dialog", "AZERTY", None, QtGui.QApplication.UnicodeUTF8)) + self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) + self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalization04finalize.py b/tests/btchippython/btchip/ui/personalization04finalize.py new file mode 100644 index 00000000..00f20d39 --- /dev/null +++ b/tests/btchippython/btchip/ui/personalization04finalize.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-04-finalize.ui' +# +# Created: Thu Aug 28 22:26:22 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 267) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(20, 20, 361, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.FinishButton = QtGui.QPushButton(Dialog) + self.FinishButton.setGeometry(QtCore.QRect(320, 230, 75, 25)) + self.FinishButton.setObjectName(_fromUtf8("FinishButton")) + self.IntroLabel_4 = QtGui.QLabel(Dialog) + self.IntroLabel_4.setGeometry(QtCore.QRect(10, 70, 351, 61)) + self.IntroLabel_4.setWordWrap(True) + self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) + self.IntroLabel_5 = QtGui.QLabel(Dialog) + self.IntroLabel_5.setGeometry(QtCore.QRect(50, 140, 121, 21)) + self.IntroLabel_5.setWordWrap(True) + self.IntroLabel_5.setObjectName(_fromUtf8("IntroLabel_5")) + self.pin1 = QtGui.QLineEdit(Dialog) + self.pin1.setGeometry(QtCore.QRect(200, 140, 181, 21)) + self.pin1.setEchoMode(QtGui.QLineEdit.Password) + self.pin1.setObjectName(_fromUtf8("pin1")) + self.remainingAttemptsLabel = QtGui.QLabel(Dialog) + self.remainingAttemptsLabel.setGeometry(QtCore.QRect(120, 170, 171, 31)) + font = QtGui.QFont() + font.setItalic(True) + self.remainingAttemptsLabel.setFont(font) + self.remainingAttemptsLabel.setWordWrap(True) + self.remainingAttemptsLabel.setObjectName(_fromUtf8("remainingAttemptsLabel")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup - security", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - completed", None, QtGui.QApplication.UnicodeUTF8)) + self.FinishButton.setText(QtGui.QApplication.translate("Dialog", "Finish", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "BTChip setup is completed. Please enter your PIN to validate it then press Finish", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_5.setText(QtGui.QApplication.translate("Dialog", "BTChip PIN :", None, QtGui.QApplication.UnicodeUTF8)) + self.remainingAttemptsLabel.setText(QtGui.QApplication.translate("Dialog", "Remaining attempts", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup01.py b/tests/btchippython/btchip/ui/personalizationseedbackup01.py new file mode 100644 index 00000000..fb1ccbe8 --- /dev/null +++ b/tests/btchippython/btchip/ui/personalizationseedbackup01.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-seedbackup-01.ui' +# +# Created: Thu Aug 28 22:26:22 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 300) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(30, 20, 351, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.NextButton = QtGui.QPushButton(Dialog) + self.NextButton.setGeometry(QtCore.QRect(320, 270, 75, 25)) + self.NextButton.setObjectName(_fromUtf8("NextButton")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(10, 100, 351, 31)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.IntroLabel_2 = QtGui.QLabel(Dialog) + self.IntroLabel_2.setGeometry(QtCore.QRect(10, 140, 351, 31)) + self.IntroLabel_2.setWordWrap(True) + self.IntroLabel_2.setObjectName(_fromUtf8("IntroLabel_2")) + self.IntroLabel_3 = QtGui.QLabel(Dialog) + self.IntroLabel_3.setGeometry(QtCore.QRect(10, 180, 351, 41)) + self.IntroLabel_3.setWordWrap(True) + self.IntroLabel_3.setObjectName(_fromUtf8("IntroLabel_3")) + self.TitleLabel_2 = QtGui.QLabel(Dialog) + self.TitleLabel_2.setGeometry(QtCore.QRect(90, 60, 251, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel_2.setFont(font) + self.TitleLabel_2.setObjectName(_fromUtf8("TitleLabel_2")) + self.IntroLabel_4 = QtGui.QLabel(Dialog) + self.IntroLabel_4.setGeometry(QtCore.QRect(10, 220, 351, 41)) + self.IntroLabel_4.setWordWrap(True) + self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) + self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "A new seed has been generated for your wallet.", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_2.setText(QtGui.QApplication.translate("Dialog", "You must backup this seed and keep it out of reach of hackers (typically by keeping it on paper).", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_3.setText(QtGui.QApplication.translate("Dialog", "You can use this seed to restore your dongle if you lose it or access your funds with any other compatible wallet.", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel_2.setText(QtGui.QApplication.translate("Dialog", "READ CAREFULLY", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "Press Next to start the backuping process.", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup02.py b/tests/btchippython/btchip/ui/personalizationseedbackup02.py new file mode 100644 index 00000000..164aa9c3 --- /dev/null +++ b/tests/btchippython/btchip/ui/personalizationseedbackup02.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-seedbackup-02.ui' +# +# Created: Thu Aug 28 22:26:22 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 300) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(30, 20, 351, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(20, 70, 351, 31)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.NextButton = QtGui.QPushButton(Dialog) + self.NextButton.setGeometry(QtCore.QRect(320, 270, 75, 25)) + self.NextButton.setObjectName(_fromUtf8("NextButton")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please disconnect the dongle then press Next", None, QtGui.QApplication.UnicodeUTF8)) + self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup03.py b/tests/btchippython/btchip/ui/personalizationseedbackup03.py new file mode 100644 index 00000000..bb106898 --- /dev/null +++ b/tests/btchippython/btchip/ui/personalizationseedbackup03.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-seedbackup-03.ui' +# +# Created: Thu Aug 28 22:26:22 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(400, 513) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(20, 10, 351, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(20, 50, 351, 61)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.IntroLabel_2 = QtGui.QLabel(Dialog) + self.IntroLabel_2.setGeometry(QtCore.QRect(20, 120, 351, 31)) + self.IntroLabel_2.setWordWrap(True) + self.IntroLabel_2.setObjectName(_fromUtf8("IntroLabel_2")) + self.IntroLabel_3 = QtGui.QLabel(Dialog) + self.IntroLabel_3.setGeometry(QtCore.QRect(20, 160, 351, 51)) + self.IntroLabel_3.setWordWrap(True) + self.IntroLabel_3.setObjectName(_fromUtf8("IntroLabel_3")) + self.IntroLabel_4 = QtGui.QLabel(Dialog) + self.IntroLabel_4.setGeometry(QtCore.QRect(20, 220, 351, 51)) + self.IntroLabel_4.setWordWrap(True) + self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) + self.IntroLabel_5 = QtGui.QLabel(Dialog) + self.IntroLabel_5.setGeometry(QtCore.QRect(20, 280, 351, 71)) + self.IntroLabel_5.setWordWrap(True) + self.IntroLabel_5.setObjectName(_fromUtf8("IntroLabel_5")) + self.IntroLabel_6 = QtGui.QLabel(Dialog) + self.IntroLabel_6.setGeometry(QtCore.QRect(20, 350, 351, 51)) + self.IntroLabel_6.setWordWrap(True) + self.IntroLabel_6.setObjectName(_fromUtf8("IntroLabel_6")) + self.IntroLabel_7 = QtGui.QLabel(Dialog) + self.IntroLabel_7.setGeometry(QtCore.QRect(20, 410, 351, 51)) + self.IntroLabel_7.setWordWrap(True) + self.IntroLabel_7.setObjectName(_fromUtf8("IntroLabel_7")) + self.NextButton = QtGui.QPushButton(Dialog) + self.NextButton.setGeometry(QtCore.QRect(310, 480, 75, 25)) + self.NextButton.setObjectName(_fromUtf8("NextButton")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "If you do not trust this computer, perform the following steps on a trusted one or a different device. Anything supporting keyboard input will work (smartphone, TV box ...)", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_2.setText(QtGui.QApplication.translate("Dialog", "Open a text editor, set the focus on the text editor, then insert the dongle", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_3.setText(QtGui.QApplication.translate("Dialog", "After a very short time, the dongle will type the seed as hexadecimal (0..9 A..F) characters, starting with \"seed\" and ending with \"X\"", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "If you perform those steps on Windows, a new device driver will be loaded the first time and the seed will not be typed. This is normal.", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_5.setText(QtGui.QApplication.translate("Dialog", "If you perform those steps on Mac, you\'ll get a popup asking you to select a keyboard type the first time and the seed will not be typed. This is normal, just close the popup.", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_6.setText(QtGui.QApplication.translate("Dialog", "If you did not see the seed for any reason, keep the focus on the text editor, unplug and plug the dongle again twice.", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel_7.setText(QtGui.QApplication.translate("Dialog", "Then press Next once you wrote the seed to a safe medium (i.e. paper) and unplugged the dongle", None, QtGui.QApplication.UnicodeUTF8)) + self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup04.py b/tests/btchippython/btchip/ui/personalizationseedbackup04.py new file mode 100644 index 00000000..e2db35f0 --- /dev/null +++ b/tests/btchippython/btchip/ui/personalizationseedbackup04.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'personalization-seedbackup-04.ui' +# +# Created: Thu Aug 28 22:26:23 2014 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName(_fromUtf8("Dialog")) + Dialog.resize(554, 190) + self.TitleLabel = QtGui.QLabel(Dialog) + self.TitleLabel.setGeometry(QtCore.QRect(30, 10, 351, 31)) + font = QtGui.QFont() + font.setPointSize(20) + font.setBold(True) + font.setItalic(True) + font.setWeight(75) + self.TitleLabel.setFont(font) + self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) + self.IntroLabel = QtGui.QLabel(Dialog) + self.IntroLabel.setGeometry(QtCore.QRect(10, 50, 351, 51)) + self.IntroLabel.setWordWrap(True) + self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) + self.seedOkButton = QtGui.QPushButton(Dialog) + self.seedOkButton.setGeometry(QtCore.QRect(20, 140, 501, 25)) + self.seedOkButton.setObjectName(_fromUtf8("seedOkButton")) + self.seedKoButton = QtGui.QPushButton(Dialog) + self.seedKoButton.setGeometry(QtCore.QRect(20, 110, 501, 25)) + self.seedKoButton.setObjectName(_fromUtf8("seedKoButton")) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) + self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) + self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Did you see the seed correctly displayed and did you backup it properly ?", None, QtGui.QApplication.UnicodeUTF8)) + self.seedOkButton.setText(QtGui.QApplication.translate("Dialog", "Yes, the seed is backed up properly and kept in a safe place, move on", None, QtGui.QApplication.UnicodeUTF8)) + self.seedKoButton.setText(QtGui.QApplication.translate("Dialog", "No, I didn\'t see the seed. Wipe the dongle and start over", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/tests/btchippython/samples/getFirmwareVersion.py b/tests/btchippython/samples/getFirmwareVersion.py new file mode 100644 index 00000000..57ffcd38 --- /dev/null +++ b/tests/btchippython/samples/getFirmwareVersion.py @@ -0,0 +1,26 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from btchip.btchip import * +import sys + +dongle = getDongle(True) +app = btchip(dongle) +print(app.getFirmwareVersion()['version']) + diff --git a/tests/btchippython/samples/runScript.py b/tests/btchippython/samples/runScript.py new file mode 100644 index 00000000..d3b283b1 --- /dev/null +++ b/tests/btchippython/samples/runScript.py @@ -0,0 +1,54 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from btchip.btchip import * +import sys +import binascii + +if len(sys.argv) < 2: + print("Usage : %s script to run" % sys.argv[0]) + sys.exit(2) + +dongle = getDongle(True) + +scriptFile = open(sys.argv[1], "r") +line = scriptFile.readline() +while line: + if (len(line) == 0) or (line[0] == '#') or (line.find('[') >= 0) or (line.find(']') >= 0): + line = scriptFile.readline() + continue + line = line.replace('\"', '') + line = line.replace(',', '') + cancelResponse = (line[0] == '!') + timeout = 10000 + if cancelResponse: + line = line[1:] + timeout = 1 + try: + line = line.strip() + if len(line) == 0: + continue + dongle.exchange(bytearray(binascii.unhexlify(line)), timeout) + except Exception: + if cancelResponse: + pass + else: + raise + line = scriptFile.readline() +scriptFile.close() diff --git a/tests/btchippython/setup.py b/tests/btchippython/setup.py new file mode 100644 index 00000000..ff0e8995 --- /dev/null +++ b/tests/btchippython/setup.py @@ -0,0 +1,31 @@ +#from distribute_setup import use_setuptools +#use_setuptools() + +from setuptools import setup, find_packages +from os.path import dirname, join + +here = dirname(__file__) +import btchip +setup( + name='btchippython', + version=btchip.__version__, + author='BTChip', + author_email='hello@ledger.fr', + description='Python library to communicate with Ledger Nano dongle', + long_description=open(join(here, 'README.md')).read(), + url='https://github.com/LedgerHQ/btchip-python', + packages=find_packages(), + install_requires=['hidapi>=0.7.99', 'ecdsa>=0.9'], + extras_require = { + 'smartcard': [ 'python-pyscard>=1.6.12-4build1' ] + }, + include_package_data=True, + zip_safe=False, + classifiers=[ + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS :: MacOS X' + ] +) + diff --git a/tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js b/tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js new file mode 100644 index 00000000..f825c5a7 --- /dev/null +++ b/tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js @@ -0,0 +1,115 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +# Coinkite co-signer provisioning +# To be run with the dongle in developer mode, PIN verified + +import hashlib +from btchip.btchip import * +from btchip.btchipUtils import * +from base64 import b64encode + +# Replace with your own seed (preferably import it and store it), key path, and Testnet flag +SEED = bytearray("fe721b95503a18a14d93914e02ff153f924737c336b01f98f2ff39395f630187".decode('hex')) +KEYPATH = "0'/2/0" +TESTNET = True + +# From Electrum + +__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +__b58base = len(__b58chars) + +def b58encode(v): + """ encode v, which is a string of bytes, to base58.""" + + long_value = 0L + for (i, c) in enumerate(v[::-1]): + long_value += (256**i) * ord(c) + + result = '' + while long_value >= __b58base: + div, mod = divmod(long_value, __b58base) + result = __b58chars[mod] + result + long_value = div + result = __b58chars[long_value] + result + + # Bitcoin does a little leading-zero-compression: + # leading 0-bytes in the input become leading-1s + nPad = 0 + for c in v: + if c == '\0': nPad += 1 + else: break + + return (__b58chars[0]*nPad) + result + +def EncodeBase58Check(vchIn): + hash = Hash(vchIn) + return b58encode(vchIn + hash[0:4]) + +def sha256(x): + return hashlib.sha256(x).digest() + + +def Hash(x): + if type(x) is unicode: x=x.encode('utf-8') + return sha256(sha256(x)) + +def i4b(self, x): + return pack('>I', x) + + +# /from Electrum + +def getXpub(publicKeyData, testnet=False): + header = ("043587CF" if testnet else "0488B21E") + result = header.decode('hex') + chr(publicKeyData['depth']) + str(publicKeyData['parentFingerprint']) + str(publicKeyData['childNumber']) + str(publicKeyData['chainCode']) + str(compress_public_key(publicKeyData['publicKey'])) + return EncodeBase58Check(result) + +def signMessage(encodedPrivateKey, data): + messageData = bytearray("\x18Bitcoin Signed Message:\n") + writeVarint(len(data), messageData) + messageData.extend(data) + messageHash = Hash(messageData) + signature = app.signImmediate(encodedPrivateKey, messageHash) + + # Parse the ASN.1 signature + + rLength = signature[3] + r = signature[4 : 4 + rLength] + sLength = signature[4 + rLength + 1] + s = signature[4 + rLength + 2:] + if rLength == 33: + r = r[1:] + if sLength == 33: + s = s[1:] + r = str(r) + s = str(s) + + # And convert it + + return b64encode(chr(27 + 4 + (signature[0] & 0x01)) + r + s) + +dongle = getDongle(True) +app = btchip(dongle) +seed = app.importPrivateKey(SEED, TESTNET) +privateKey = app.deriveBip32Key(seed, KEYPATH) +publicKeyData = app.getPublicKey(privateKey) +print getXpub(publicKeyData, TESTNET) +print signMessage(privateKey, "Coinkite") +dongle.close() diff --git a/tests/btchippython/tests/coinkite-cosigning/testSignDev.js b/tests/btchippython/tests/coinkite-cosigning/testSignDev.js new file mode 100644 index 00000000..03cd78ef --- /dev/null +++ b/tests/btchippython/tests/coinkite-cosigning/testSignDev.js @@ -0,0 +1,168 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +# Coinkite co-signer signing +# To be run with the dongle in developer mode, PIN verified +# Pass the JSON request to sign as parameter + +# TODO : verify Coinkite signature in the request + +import hashlib +import sys +import json +from btchip.btchip import * +from btchip.btchipUtils import * +from base64 import b64encode + +# Replace with your own seed (preferably import it and store it), key path, and Testnet flag +SEED = bytearray("fe721b95503a18a14d93914e02ff153f924737c336b01f98f2ff39395f630187".decode('hex')) +KEYPATH = "0'/2/0" +TESTNET = True + + +# From Electrum + +__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +__b58base = len(__b58chars) + +def b58encode(v): + """ encode v, which is a string of bytes, to base58.""" + + long_value = 0L + for (i, c) in enumerate(v[::-1]): + long_value += (256**i) * ord(c) + + result = '' + while long_value >= __b58base: + div, mod = divmod(long_value, __b58base) + result = __b58chars[mod] + result + long_value = div + result = __b58chars[long_value] + result + + # Bitcoin does a little leading-zero-compression: + # leading 0-bytes in the input become leading-1s + nPad = 0 + for c in v: + if c == '\0': nPad += 1 + else: break + + return (__b58chars[0]*nPad) + result + +def EncodeBase58Check(vchIn): + hash = Hash(vchIn) + return b58encode(vchIn + hash[0:4]) + +def sha256(x): + return hashlib.sha256(x).digest() + +def hash_160(public_key): + try: + md = hashlib.new('ripemd160') + md.update(sha256(public_key)) + return md.digest() + except Exception: + import ripemd + md = ripemd.new(sha256(public_key)) + return md.digest() + +def Hash(x): + if type(x) is unicode: x=x.encode('utf-8') + return sha256(sha256(x)) + +def i4b(self, x): + return pack('>I', x) + + +# /from Electrum + +def getXpub(publicKeyData, testnet=False): + header = ("043587CF" if testnet else "0488B21E") + result = header.decode('hex') + chr(publicKeyData['depth']) + str(publicKeyData['parentFingerprint']) + str(publicKeyData['childNumber']) + str(publicKeyData['chainCode']) + str(compress_public_key(publicKeyData['publicKey'])) + return EncodeBase58Check(result) + +def getPub(publicKeyData, testnet=False): + header = ("6F" if testnet else "00") + keyData = hash_160(str(compress_public_key(publicKeyData))) + return EncodeBase58Check(header.decode('hex') + keyData) + +def signMessage(encodedPrivateKey, data): + messageData = bytearray("\x18Bitcoin Signed Message:\n") + writeVarint(len(data), messageData) + messageData.extend(data) + messageHash = Hash(messageData) + signature = app.signImmediate(encodedPrivateKey, messageHash) + + # Parse the ASN.1 signature + + rLength = signature[3] + r = signature[4 : 4 + rLength] + sLength = signature[4 + rLength + 1] + s = signature[4 + rLength + 2:] + if rLength == 33: + r = r[1:] + if sLength == 33: + s = s[1:] + r = str(r) + s = str(s) + + # And convert it + + return b64encode(chr(27 + 4 + (signature[0] & 0x01)) + r + s) + + +f = open(sys.argv[1], 'r') +signData = json.load(f) +requestData = json.loads(signData['contents']) + +result = {} +result['cosigner'] = requestData['cosigner'] +result['request'] = requestData['request'] +result['signatures'] = [] + +dongle = getDongle(True) +app = btchip(dongle) +seed = app.importPrivateKey(SEED, TESTNET) +privateKey = app.deriveBip32Key(seed, KEYPATH) +publicKeyData = app.getPublicKey(privateKey) + +wallets = {} +for key in requestData['req_keys'].keys(): + privateKeyDiv = app.deriveBip32Key(privateKey, key) + publicKeyDataDiv = app.getPublicKey(privateKeyDiv) + if getPub(publicKeyDataDiv['publicKey'], TESTNET) == requestData['req_keys'][key][0]: + wallets[key] = privateKeyDiv + else: + raise "Invalid wallet, could not match key" + +for signInput in requestData['inputs']: + sigHash = signInput[1].decode('hex') + signature = "\x30" + str(app.signImmediate(wallets[signInput[0]], sigHash)[1:]) + signature = signature + "\x01" + result['signatures'].append([signature.encode('hex'), sigHash.encode('hex'), signInput[0]]) + +body = {} +body['_humans'] = "Upload this set of signatures to Coinkite." +body['content'] = json.dumps(result) +body['signature'] = signMessage(privateKey, body['content']) +body['signed_by'] = getPub(publicKeyData['publicKey'], False) # Bug, should use network + +print json.dumps(body) + +dongle.close() + diff --git a/tests/btchippython/tests/testConnectivity.py b/tests/btchippython/tests/testConnectivity.py new file mode 100644 index 00000000..85ab29f9 --- /dev/null +++ b/tests/btchippython/tests/testConnectivity.py @@ -0,0 +1,34 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from btchip.btchip import * +#from btchip.btchipUtils import * + +# Run on any dongle to test connectivity. + +dongle = getDongle(True) +app = btchip(dongle) + +print('btchip firmware version:') +print(app.getFirmwareVersion()) + +print('some random number from the dongle:') +print(map(hex, app.getRandom(20))) + +dongle.close() diff --git a/tests/btchippython/tests/testMessageSignature.py b/tests/btchippython/tests/testMessageSignature.py new file mode 100644 index 00000000..7d041ed1 --- /dev/null +++ b/tests/btchippython/tests/testMessageSignature.py @@ -0,0 +1,56 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from btchip.btchip import * +from btchip.btchipUtils import * + +# Run on non configured dongle or dongle configured with test seed below + +SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284E88F10C57CF569BB3DADF7B1027F2D".decode('hex')) + +MESSAGE = "Campagne de Sarkozy : une double comptabilite chez Bygmalion" + +SECONDFACTOR_1 = "Powercycle then confirm signature of .Campagne de Sarkozy : une double comptabilite chez Bygmalion. for address 17JusYNVXLPm3hBPzzRQkARYDMUBgRUMVc with PIN" +SIGNATURE = bytearray("30450221009a0d28391c0535aec1077bbb86614c8f3c384a3e9aa1a124bfb9ce9649196b7e02200efa1adc010a7bdde4784ee98441e402f93b3c50a2760cb09dda07501e02c81f".decode('hex')) + +# Optional setup +dongle = getDongle(True) +app = btchip(dongle) +try: + app.setup(btchip.OPERATION_MODE_WALLET, btchip.FEATURE_RFC6979, 0x00, 0x05, "1234", None, btchip.QWERTY_KEYMAP, SEED) +except Exception: + pass +# Authenticate +app.verifyPin("1234") +# Start signing +app.signMessagePrepare("0'/0/0", MESSAGE) +dongle.close() +# Wait for the second factor confirmation +# Done on the same application for test purposes, this is typically done in another window +# or another computer for bigger transactions +response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") +if not response.startswith(SECONDFACTOR_1): + raise BTChipException("Invalid second factor") +# Get a reference to the dongle again, as it was disconnected +dongle = getDongle(True) +app = btchip(dongle) +# Compute the signature +signature = app.signMessageSign(response[len(response) - 4:]) +if signature <> SIGNATURE: + raise BTChipException("Invalid signature") diff --git a/tests/btchippython/tests/testMultisigArmory.py b/tests/btchippython/tests/testMultisigArmory.py new file mode 100644 index 00000000..c68bfb53 --- /dev/null +++ b/tests/btchippython/tests/testMultisigArmory.py @@ -0,0 +1,194 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from btchip.btchip import * +from btchip.btchipUtils import * +import json + +""" +Signs a TX generated by Armory. That TX: + +{ + 'inputs': [{ + 'p2shscript': '52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae', + 'supporttxhash_be': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', + 'sequence': 4294967295, + 'keys': [{ + 'dersighex': '', + 'pubkeyhex': '0269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c6283', + 'wltlochex': '' + }, { + 'dersighex': '', + 'pubkeyhex': '02f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb5', + 'wltlochex': '' + }, { + 'dersighex': '', + 'pubkeyhex': '037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b9102', + 'wltlochex': '' + }], + 'contriblabel': u '', + 'supporttxhash_le': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c', + 'contribid': 'JLBercZk', + 'version': 1, + 'inputvalue': 46000000, + 'outpoint': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000', + 'magicbytes': '0b110907', + 'supporttx': '01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000', + 'numkeys': 3, + 'supporttxhash': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', + 'supporttxoutindex': 0 + }], + 'fee': 10000, + 'locktimeint': 0, + 'outputs': [{ + 'txoutvalue': 10000000, + 'authdata': '', + 'contriblabel': '', + 'p2shscript': '', + 'scripttypeint': 4, + 'isp2sh': True, + 'txoutscript': 'a914c0c3b6ada732c797881d00de6c350eec96e3d22287', + 'authmethod': 'NONE', + 'hasaddrstr': True, + 'contribid': '', + 'version': 1, + 'ismultisig': False, + 'magicbytes': '0b110907', + 'addrstr': '2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8', + 'scripttypestr': 'Standard (P2SH)', + 'wltlocator': '' + }, { + 'txoutvalue': 35990000, + 'authdata': '', + 'contriblabel': '', + 'p2shscript': '', + 'scripttypeint': 4, + 'isp2sh': True, + 'txoutscript': 'a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387', + 'authmethod': 'NONE', + 'hasaddrstr': True, + 'contribid': '', + 'version': 1, + 'ismultisig': False, + 'magicbytes': '0b110907', + 'addrstr': '2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1', + 'scripttypestr': 'Standard (P2SH)', + 'wltlocator': '' + }], + 'sumoutputs': 45990000, + 'suminputs': 46000000, + 'version': 1, + 'numoutputs': 2, + 'magicbytes': '0b110907', + 'locktimedate': '', + 'locktimeblock': 0, + 'id': '8jkccikU', + 'numinputs': 1 +} + +Input comes from vout[0] of 0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300. +TX I want to generate is 0.10 to 2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8 + +The multisig address 2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1 contains 0.46 BTC, and is generated +using the public keys 0'/0/0, 0'/0/1, and 0'/0/2 from the seed below. + +""" + +# Run on non configured dongle or dongle configured with test seed below + +SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284C88A10C57CF569BB3DADF7B1027F2D".decode('hex')) + +# Armory supporttx +UTX = bytearray("01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000".decode('hex')) +UTXO_INDEX = 0 +OUTPUT = bytearray("02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex')) +# Armory p2shscript +REDEEMSCRIPT = bytearray("52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae".decode('hex')) + +SIGNATURE_0 = bytearray("3044022056cb1b781fd04cfe6c04756ad56d02e5512f3fe7f411bc22d1594da5c815a393022074ad7f4d47af7c3f8a7ddf0ba2903f986a88649b0018ce1538c379b304a6a23801".decode('hex')) +SIGNATURE_1 = bytearray("304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a5026001".decode('hex')) +SIGNATURE_2 = bytearray("30440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db01".decode('hex')) +TRANSACTION = bytearray("0100000001008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000fc004730440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db0147304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a50260014c6952210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253aeffffffff02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c38700000000".decode('hex')) + +SECONDFACTOR_1 = "RELAXED MODE Powercycle then confirm use of 0.46 BTC with PIN" + +# Armory txoutscript +output = get_output_script([["0.1", bytearray("a914c0c3b6ada732c797881d00de6c350eec96e3d22287".decode('hex'))], ["0.3599", bytearray("a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex'))]]); +if output<>OUTPUT: + raise BTChipException("Invalid output script encoding"); + +# Optional setup +dongle = getDongle(True) +app = btchip(dongle) +try: + app.setup(btchip.OPERATION_MODE_RELAXED_WALLET, btchip.FEATURE_RFC6979, 111, 196, "1234", None, btchip.QWERTY_KEYMAP, SEED) +except Exception: + pass +# Authenticate +app.verifyPin("1234") +# Get the trusted input associated to the UTXO +transaction = bitcoinTransaction(UTX) +print transaction +trustedInput = app.getTrustedInput(transaction, UTXO_INDEX) +# Start composing the transaction +app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) +app.finalizeInputFull(OUTPUT) +dongle.close() +# Wait for the second factor confirmation +# Done on the same application for test purposes, this is typically done in another window +# or another computer for bigger transactions +response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") +if not response.startswith(SECONDFACTOR_1): + raise BTChipException("Invalid second factor") +# Get a reference to the dongle again, as it was disconnected +dongle = getDongle(True) +app = btchip(dongle) +# Replay the transaction, this time continue it since the second factor is ready +app.startUntrustedTransaction(False, 0, [trustedInput], REDEEMSCRIPT) +app.finalizeInputFull(OUTPUT) +# Provide the second factor to finalize the signature +signature1 = app.untrustedHashSign("0'/0/1", response[len(response) - 4:]) +if signature1 <> SIGNATURE_1: + raise BTChipException("Invalid signature1") + +# Same thing for the second signature + +app.verifyPin("1234") +app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) +app.finalizeInputFull(OUTPUT) +dongle.close() +response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") +if not response.startswith(SECONDFACTOR_1): + raise BTChipException("Invalid second factor") +dongle = getDongle(True) +app = btchip(dongle) +app.startUntrustedTransaction(False, 0, [trustedInput], REDEEMSCRIPT) +app.finalizeInputFull(OUTPUT) +signature2 = app.untrustedHashSign("0'/0/2", response[len(response) - 4:]) +if signature2 <> SIGNATURE_2: + raise BTChipException("Invalid signature2") + +# Finalize the transaction - build the redeem script and put everything together +inputScript = get_p2sh_input_script(REDEEMSCRIPT, [signature2, signature1]) +transaction = format_transaction(OUTPUT, [ [ trustedInput['value'], inputScript] ]) +print "Generated transaction : " + str(transaction).encode('hex') +if transaction <> TRANSACTION: + raise BTChipException("Invalid transaction") +# The transaction is ready to be broadcast, enjoy + diff --git a/tests/btchippython/tests/testMultisigArmoryNo2FA.py b/tests/btchippython/tests/testMultisigArmoryNo2FA.py new file mode 100644 index 00000000..ff7c5b25 --- /dev/null +++ b/tests/btchippython/tests/testMultisigArmoryNo2FA.py @@ -0,0 +1,169 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from btchip.btchip import * +from btchip.btchipUtils import * +import json + +""" +Signs a TX generated by Armory. That TX: + +{ + 'inputs': [{ + 'p2shscript': '52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae', + 'supporttxhash_be': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', + 'sequence': 4294967295, + 'keys': [{ + 'dersighex': '', + 'pubkeyhex': '0269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c6283', + 'wltlochex': '' + }, { + 'dersighex': '', + 'pubkeyhex': '02f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb5', + 'wltlochex': '' + }, { + 'dersighex': '', + 'pubkeyhex': '037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b9102', + 'wltlochex': '' + }], + 'contriblabel': u '', + 'supporttxhash_le': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c', + 'contribid': 'JLBercZk', + 'version': 1, + 'inputvalue': 46000000, + 'outpoint': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000', + 'magicbytes': '0b110907', + 'supporttx': '01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000', + 'numkeys': 3, + 'supporttxhash': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', + 'supporttxoutindex': 0 + }], + 'fee': 10000, + 'locktimeint': 0, + 'outputs': [{ + 'txoutvalue': 10000000, + 'authdata': '', + 'contriblabel': '', + 'p2shscript': '', + 'scripttypeint': 4, + 'isp2sh': True, + 'txoutscript': 'a914c0c3b6ada732c797881d00de6c350eec96e3d22287', + 'authmethod': 'NONE', + 'hasaddrstr': True, + 'contribid': '', + 'version': 1, + 'ismultisig': False, + 'magicbytes': '0b110907', + 'addrstr': '2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8', + 'scripttypestr': 'Standard (P2SH)', + 'wltlocator': '' + }, { + 'txoutvalue': 35990000, + 'authdata': '', + 'contriblabel': '', + 'p2shscript': '', + 'scripttypeint': 4, + 'isp2sh': True, + 'txoutscript': 'a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387', + 'authmethod': 'NONE', + 'hasaddrstr': True, + 'contribid': '', + 'version': 1, + 'ismultisig': False, + 'magicbytes': '0b110907', + 'addrstr': '2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1', + 'scripttypestr': 'Standard (P2SH)', + 'wltlocator': '' + }], + 'sumoutputs': 45990000, + 'suminputs': 46000000, + 'version': 1, + 'numoutputs': 2, + 'magicbytes': '0b110907', + 'locktimedate': '', + 'locktimeblock': 0, + 'id': '8jkccikU', + 'numinputs': 1 +} + +Input comes from vout[0] of 0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300. +TX I want to generate is 0.10 to 2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8 + +The multisig address 2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1 contains 0.46 BTC, and is generated +using the public keys 0'/0/0, 0'/0/1, and 0'/0/2 from the seed below. + +""" + +# Run on non configured dongle or dongle configured with test seed below + +SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284C88A10C57CF569BB3DADF7B1027F2D".decode('hex')) + +UTX = bytearray("01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000".decode('hex')) +UTXO_INDEX = 0 +OUTPUT = bytearray("02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex')) +# Armory p2shscript +REDEEMSCRIPT = bytearray("52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae".decode('hex')) + +SIGNATURE_0 = bytearray("3044022056cb1b781fd04cfe6c04756ad56d02e5512f3fe7f411bc22d1594da5c815a393022074ad7f4d47af7c3f8a7ddf0ba2903f986a88649b0018ce1538c379b304a6a23801".decode('hex')) +SIGNATURE_1 = bytearray("304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a5026001".decode('hex')) +SIGNATURE_2 = bytearray("30440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db01".decode('hex')) +# Armory supporttx +TRANSACTION = bytearray("0100000001008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000fc004730440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db0147304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a50260014c6952210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253aeffffffff02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c38700000000".decode('hex')) + +# Armory txoutscript +output = get_output_script([["0.1", bytearray("a914c0c3b6ada732c797881d00de6c350eec96e3d22287".decode('hex'))], ["0.3599", bytearray("a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex'))]]); +if output<>OUTPUT: + raise BTChipException("Invalid output script encoding"); + +# Optional setup +dongle = getDongle(True) +app = btchip(dongle) +try: + app.setup(btchip.OPERATION_MODE_RELAXED_WALLET, btchip.FEATURE_RFC6979|btchip.FEATURE_NO_2FA_P2SH, 111, 196, "1234", None, btchip.QWERTY_KEYMAP, SEED) +except Exception: + pass +# Authenticate +app.verifyPin("1234") +# Get the trusted input associated to the UTXO +transaction = bitcoinTransaction(UTX) +print transaction +trustedInput = app.getTrustedInput(transaction, UTXO_INDEX) +# Start composing the transaction +app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) +app.finalizeInputFull(OUTPUT) +signature1 = app.untrustedHashSign("0'/0/1", "") +if signature1 <> SIGNATURE_1: + raise BTChipException("Invalid signature1") + +# Same thing for the second signature + +app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) +app.finalizeInputFull(OUTPUT) +signature2 = app.untrustedHashSign("0'/0/2", "") +if signature2 <> SIGNATURE_2: + raise BTChipException("Invalid signature2") + +# Finalize the transaction - build the redeem script and put everything together +inputScript = get_p2sh_input_script(REDEEMSCRIPT, [signature2, signature1]) +transaction = format_transaction(OUTPUT, [ [ trustedInput['value'], inputScript] ]) +print "Generated transaction : " + str(transaction).encode('hex') +if transaction <> TRANSACTION: + raise BTChipException("Invalid transaction") +# The transaction is ready to be broadcast, enjoy + diff --git a/tests/btchippython/tests/testSimpleTransaction.py b/tests/btchippython/tests/testSimpleTransaction.py new file mode 100644 index 00000000..3b2abffd --- /dev/null +++ b/tests/btchippython/tests/testSimpleTransaction.py @@ -0,0 +1,78 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from btchip.btchip import * +from btchip.btchipUtils import * + +# Run on non configured dongle or dongle configured with test seed below + +SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284E88F10C57CF569BB3DADF7B1027F2D".decode('hex')) + +UTX = bytearray("01000000014ea60aeac5252c14291d428915bd7ccd1bfc4af009f4d4dc57ae597ed0420b71010000008a47304402201f36a12c240dbf9e566bc04321050b1984cd6eaf6caee8f02bb0bfec08e3354b022012ee2aeadcbbfd1e92959f57c15c1c6debb757b798451b104665aa3010569b49014104090b15bde569386734abf2a2b99f9ca6a50656627e77de663ca7325702769986cf26cc9dd7fdea0af432c8e2becc867c932e1b9dd742f2a108997c2252e2bdebffffffff0281b72e00000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88aca0860100000000001976a9144533f5fb9b4817f713c48f0bfe96b9f50c476c9b88ac00000000".decode('hex')) +UTXO_INDEX = 1 +ADDRESS = "1BTChipvU14XH6JdRiK9CaenpJ2kJR9RnC" +AMOUNT = "0.0009" +FEES = "0.0001" + +SECONDFACTOR_1 = "Powercycle then confirm transfer of 0.0009 BTC to 1BTChipvU14XH6JdRiK9CaenpJ2kJR9RnC fees 0.0001 BTC change 0 BTC with PIN" +SIGNATURE = bytearray("3045022100ea6df031b47629590daf5598b6f0680ad0132d8953b401577f01e8cc46393fe602202201b7a19d706a0213dcfeb7033719b92c6fd58a2d1d53411de71c4d8353154b01".decode('hex')) +TRANSACTION = bytearray("0100000001c773da236484dae8f0fdba3d7e0ba1d05070d1a34fc44943e638441262a04f10010000006b483045022100ea6df031b47629590daf5598b6f0680ad0132d8953b401577f01e8cc46393fe602202201b7a19d706a0213dcfeb7033719b92c6fd58a2d1d53411de71c4d8353154b01210348bb1fade0adde1bf202726e6db5eacd2063fce7ecf8bbfd17377f09218d5814ffffffff01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac00000000".decode('hex')) + +# Optional setup +dongle = getDongle(True) +app = btchip(dongle) +try: + app.setup(btchip.OPERATION_MODE_WALLET, btchip.FEATURE_RFC6979, 0x00, 0x05, "1234", None, btchip.QWERTY_KEYMAP, SEED) +except Exception: + pass +# Authenticate +app.verifyPin("1234") +# Get the public key and compress it +publicKey = compress_public_key(app.getWalletPublicKey("0'/0/0")['publicKey']) +# Get the trusted input associated to the UTXO +transaction = bitcoinTransaction(UTX) +outputScript = transaction.outputs[UTXO_INDEX].script +trustedInput = app.getTrustedInput(transaction, UTXO_INDEX) +# Start composing the transaction +app.startUntrustedTransaction(True, 0, [trustedInput], outputScript) +outputData = app.finalizeInput(ADDRESS, AMOUNT, FEES, "0'/1/0") +dongle.close() +# Wait for the second factor confirmation +# Done on the same application for test purposes, this is typically done in another window +# or another computer for bigger transactions +response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") +if not response.startswith(SECONDFACTOR_1): + raise BTChipException("Invalid second factor") +# Get a reference to the dongle again, as it was disconnected +dongle = getDongle(True) +app = btchip(dongle) +# Replay the transaction, this time continue it since the second factor is ready +app.startUntrustedTransaction(False, 0, [trustedInput], outputScript) +app.finalizeInput(ADDRESS, "0.0009", "0.0001", "0'/1/0") +# Provide the second factor to finalize the signature +signature = app.untrustedHashSign("0'/0/0", response[len(response) - 4:]) +if signature <> SIGNATURE: + raise BTChipException("Invalid signature") +# Finalize the transaction - build the redeem script and put everything together +inputScript = get_regular_input_script(signature, publicKey) +transaction = format_transaction(outputData['outputData'], [ [ trustedInput['value'], inputScript] ]) +print "Generated transaction : " + str(transaction).encode('hex') +if transaction <> TRANSACTION: + raise BTChipException("Invalid transaction") +# The transaction is ready to be broadcast, enjoy diff --git a/tests/btchippython/ui/make.sh b/tests/btchippython/ui/make.sh new file mode 100755 index 00000000..24119f95 --- /dev/null +++ b/tests/btchippython/ui/make.sh @@ -0,0 +1,12 @@ +#!/bin/bash +pyuic4 personalization-00-start.ui -o ../btchip/ui/personalization00start.py +pyuic4 personalization-01-seed.ui -o ../btchip/ui/personalization01seed.py +pyuic4 personalization-02-security.ui -o ../btchip/ui/personalization02security.py +pyuic4 personalization-03-config.ui -o ../btchip/ui/personalization03config.py +pyuic4 personalization-04-finalize.ui -o ../btchip/ui/personalization04finalize.py +pyuic4 personalization-seedbackup-01.ui -o ../btchip/ui/personalizationseedbackup01.py +pyuic4 personalization-seedbackup-02.ui -o ../btchip/ui/personalizationseedbackup02.py +pyuic4 personalization-seedbackup-03.ui -o ../btchip/ui/personalizationseedbackup03.py +pyuic4 personalization-seedbackup-04.ui -o ../btchip/ui/personalizationseedbackup04.py + + diff --git a/tests/btchippython/ui/personalization-00-start.ui b/tests/btchippython/ui/personalization-00-start.ui new file mode 100644 index 00000000..13cc3e2f --- /dev/null +++ b/tests/btchippython/ui/personalization-00-start.ui @@ -0,0 +1,98 @@ + + + Dialog + + + + 0 + 0 + 400 + 231 + + + + BTChip setup + + + + + 120 + 20 + 231 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup + + + + + + 20 + 60 + 351 + 61 + + + + Your BTChip dongle is not set up - you'll be able to create a new wallet, or restore an existing one, and choose your security profile. + + + true + + + + + + 310 + 200 + 75 + 25 + + + + Next + + + + + + 20 + 120 + 351 + 81 + + + + Sensitive information including your dongle PIN will be exchanged during this setup phase - it is recommended to execute it on a secure computer, disconnected from any network, especially if you restore a wallet backup. + + + true + + + + + + 20 + 200 + 75 + 25 + + + + Cancel + + + + + + diff --git a/tests/btchippython/ui/personalization-01-seed.ui b/tests/btchippython/ui/personalization-01-seed.ui new file mode 100644 index 00000000..15ae4ce9 --- /dev/null +++ b/tests/btchippython/ui/personalization-01-seed.ui @@ -0,0 +1,160 @@ + + + Dialog + + + + 0 + 0 + 400 + 300 + + + + BTChip setup - seed + + + + + 50 + 20 + 311 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - seed (1/3) + + + + + + 20 + 60 + 351 + 61 + + + + Please select an option : either create a new wallet or restore an existing one + + + true + + + + + + 20 + 130 + 94 + 21 + + + + New Wallet + + + true + + + buttonGroup + + + + + + 20 + 180 + 171 + 21 + + + + Restore wallet backup + + + buttonGroup + + + + + false + + + + 50 + 210 + 331 + 21 + + + + QLineEdit::Normal + + + Enter an hexadecimal seed or a BIP 39 mnemonic code + + + + + + 10 + 270 + 75 + 25 + + + + Cancel + + + + + + 320 + 270 + 75 + 25 + + + + Next + + + + + + 130 + 240 + 171 + 31 + + + + + true + + + + Mnemonic API is not available + + + true + + + + + + + + + diff --git a/tests/btchippython/ui/personalization-02-security.ui b/tests/btchippython/ui/personalization-02-security.ui new file mode 100644 index 00000000..e0f0d944 --- /dev/null +++ b/tests/btchippython/ui/personalization-02-security.ui @@ -0,0 +1,226 @@ + + + Dialog + + + + 0 + 0 + 400 + 503 + + + + BTChip setup - security + + + + + 20 + 20 + 361 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - security (2/3) + + + + + + 10 + 60 + 351 + 61 + + + + Please choose a security profile + + + true + + + + + + 20 + 110 + 81 + 21 + + + + Hardened + + + true + + + buttonGroup + + + + + + 20 + 210 + 81 + 21 + + + + PIN only + + + buttonGroup + + + + + + 50 + 140 + 351 + 61 + + + + You need to remove the dongle and insert it again to get a second factor validation of all operations. Recommended for expert users and to be fully protected against malwares. + + + true + + + + + + 50 + 230 + 351 + 61 + + + + You only need to enter a PIN once when inserting the dongle. Transactions are not protected against malwares + + + true + + + + + + 10 + 470 + 75 + 25 + + + + Cancel + + + + + + 310 + 470 + 75 + 25 + + + + Next + + + + + + 10 + 300 + 351 + 61 + + + + Please choose a PIN associated to the BTChip dongle. The PIN protects the dongle in case it is stolen, and can be up to 32 characters. The dongle is wiped if a wrong PIN is entered 3 times in a row. + + + true + + + + + + 20 + 380 + 161 + 31 + + + + Enter the new PIN : + + + true + + + + + + 210 + 380 + 161 + 21 + + + + QLineEdit::Password + + + + + + 210 + 420 + 161 + 21 + + + + QLineEdit::Password + + + + + + 20 + 420 + 171 + 31 + + + + Repeat the new PIN : + + + true + + + + + + + + + diff --git a/tests/btchippython/ui/personalization-03-config.ui b/tests/btchippython/ui/personalization-03-config.ui new file mode 100644 index 00000000..8332dd9e --- /dev/null +++ b/tests/btchippython/ui/personalization-03-config.ui @@ -0,0 +1,136 @@ + + + Dialog + + + + 0 + 0 + 400 + 243 + + + + BTChip setup + + + + + 30 + 10 + 361 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - config (3/3) + + + + + + 20 + 50 + 351 + 61 + + + + Please select your keyboard type to type the second factor confirmation + + + true + + + + + + 50 + 110 + 94 + 21 + + + + QWERTY + + + true + + + keyboardGroup + + + + + + 50 + 140 + 94 + 21 + + + + QWERTZ + + + keyboardGroup + + + + + + 50 + 170 + 94 + 21 + + + + AZERTY + + + keyboardGroup + + + + + + 10 + 210 + 75 + 25 + + + + Cancel + + + + + + 320 + 210 + 75 + 25 + + + + Next + + + + + + + + + diff --git a/tests/btchippython/ui/personalization-04-finalize.ui b/tests/btchippython/ui/personalization-04-finalize.ui new file mode 100644 index 00000000..f3093b5b --- /dev/null +++ b/tests/btchippython/ui/personalization-04-finalize.ui @@ -0,0 +1,122 @@ + + + Dialog + + + + 0 + 0 + 400 + 267 + + + + BTChip setup - security + + + + + 20 + 20 + 361 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - completed + + + + + + 320 + 230 + 75 + 25 + + + + Finish + + + + + + 10 + 70 + 351 + 61 + + + + BTChip setup is completed. Please enter your PIN to validate it then press Finish + + + true + + + + + + 50 + 140 + 121 + 21 + + + + BTChip PIN : + + + true + + + + + + 200 + 140 + 181 + 21 + + + + QLineEdit::Password + + + + + + 120 + 170 + 171 + 31 + + + + + true + + + + Remaining attempts + + + true + + + + + + + + + diff --git a/tests/btchippython/ui/personalization-seedbackup-01.ui b/tests/btchippython/ui/personalization-seedbackup-01.ui new file mode 100644 index 00000000..f986c035 --- /dev/null +++ b/tests/btchippython/ui/personalization-seedbackup-01.ui @@ -0,0 +1,138 @@ + + + Dialog + + + + 0 + 0 + 400 + 300 + + + + BTChip setup + + + + + 30 + 20 + 351 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - seed backup + + + + + + 320 + 270 + 75 + 25 + + + + Next + + + + + + 10 + 100 + 351 + 31 + + + + A new seed has been generated for your wallet. + + + true + + + + + + 10 + 140 + 351 + 31 + + + + You must backup this seed and keep it out of reach of hackers (typically by keeping it on paper). + + + true + + + + + + 10 + 180 + 351 + 41 + + + + You can use this seed to restore your dongle if you lose it or access your funds with any other compatible wallet. + + + true + + + + + + 90 + 60 + 251 + 31 + + + + + 20 + 75 + true + true + + + + READ CAREFULLY + + + + + + 10 + 220 + 351 + 41 + + + + Press Next to start the backuping process. + + + true + + + + + + diff --git a/tests/btchippython/ui/personalization-seedbackup-02.ui b/tests/btchippython/ui/personalization-seedbackup-02.ui new file mode 100644 index 00000000..4260ab15 --- /dev/null +++ b/tests/btchippython/ui/personalization-seedbackup-02.ui @@ -0,0 +1,69 @@ + + + Dialog + + + + 0 + 0 + 400 + 300 + + + + BTChip setup + + + + + 30 + 20 + 351 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - seed backup + + + + + + 20 + 70 + 351 + 31 + + + + Please disconnect the dongle then press Next + + + true + + + + + + 320 + 270 + 75 + 25 + + + + Next + + + + + + diff --git a/tests/btchippython/ui/personalization-seedbackup-03.ui b/tests/btchippython/ui/personalization-seedbackup-03.ui new file mode 100644 index 00000000..edc69bcb --- /dev/null +++ b/tests/btchippython/ui/personalization-seedbackup-03.ui @@ -0,0 +1,165 @@ + + + Dialog + + + + 0 + 0 + 400 + 513 + + + + BTChip setup + + + + + 20 + 10 + 351 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - seed backup + + + + + + 20 + 50 + 351 + 61 + + + + If you do not trust this computer, perform the following steps on a trusted one or a different device. Anything supporting keyboard input will work (smartphone, TV box ...) + + + true + + + + + + 20 + 120 + 351 + 31 + + + + Open a text editor, set the focus on the text editor, then insert the dongle + + + true + + + + + + 20 + 160 + 351 + 51 + + + + After a very short time, the dongle will type the seed as hexadecimal (0..9 A..F) characters, starting with "seed" and ending with "X" + + + true + + + + + + 20 + 220 + 351 + 51 + + + + If you perform those steps on Windows, a new device driver will be loaded the first time and the seed will not be typed. This is normal. + + + true + + + + + + 20 + 280 + 351 + 71 + + + + If you perform those steps on Mac, you'll get a popup asking you to select a keyboard type the first time and the seed will not be typed. This is normal, just close the popup. + + + true + + + + + + 20 + 350 + 351 + 51 + + + + If you did not see the seed for any reason, keep the focus on the text editor, unplug and plug the dongle again twice. + + + true + + + + + + 20 + 410 + 351 + 51 + + + + Then press Next once you wrote the seed to a safe medium (i.e. paper) and unplugged the dongle + + + true + + + + + + 310 + 480 + 75 + 25 + + + + Next + + + + + + diff --git a/tests/btchippython/ui/personalization-seedbackup-04.ui b/tests/btchippython/ui/personalization-seedbackup-04.ui new file mode 100644 index 00000000..9662eb76 --- /dev/null +++ b/tests/btchippython/ui/personalization-seedbackup-04.ui @@ -0,0 +1,82 @@ + + + Dialog + + + + 0 + 0 + 554 + 190 + + + + BTChip setup + + + + + 30 + 10 + 351 + 31 + + + + + 20 + 75 + true + true + + + + BTChip setup - seed backup + + + + + + 10 + 50 + 351 + 51 + + + + Did you see the seed correctly displayed and did you backup it properly ? + + + true + + + + + + 20 + 140 + 501 + 25 + + + + Yes, the seed is backed up properly and kept in a safe place, move on + + + + + + 20 + 110 + 501 + 25 + + + + No, I didn't see the seed. Wipe the dongle and start over + + + + + + diff --git a/tests/electrum_clone/electrumravencoin b/tests/electrum_clone/electrumravencoin new file mode 160000 index 00000000..d856e264 --- /dev/null +++ b/tests/electrum_clone/electrumravencoin @@ -0,0 +1 @@ +Subproject commit d856e2649b367297afb608e82bb9d98111acc49a diff --git a/tests/electrum_clone/ledger_sign_funcs.py b/tests/electrum_clone/ledger_sign_funcs.py new file mode 100644 index 00000000..7591ee61 --- /dev/null +++ b/tests/electrum_clone/ledger_sign_funcs.py @@ -0,0 +1,84 @@ +from btchippython.btchip.bitcoinTransaction import bitcoinTransaction +from btchippython.btchip.btchip import btchip +from electrum_clone.electrumravencoin.electrum.transaction import Transaction +from electrum_clone.electrumravencoin.electrum.util import bfh +from electrum_clone.electrumravencoin.electrum.ravencoin import int_to_hex, var_int + +def sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath): + inputs = [] + chipInputs = [] + redeemScripts = [] + output = None + p2shTransaction = False + segwitTransaction = False + pin = "" + + print("SIGNING") + + # Fetch inputs of the transaction to sign + for i, txin in enumerate(tx.inputs()): + redeemScript = Transaction.get_preimage_script(txin) + txin_prev_tx = txin.utxo + txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None + print((txin_prev_tx_raw, + txin.prevout.out_idx, + redeemScript, + txin.prevout.txid.hex(), + pubkeys[i], + txin.nsequence, + txin.value_sats())) + inputs.append([txin_prev_tx_raw, + txin.prevout.out_idx, + redeemScript, + txin.prevout.txid.hex(), + pubkeys[i], + txin.nsequence, + txin.value_sats()]) + + txOutput = var_int(len(tx.outputs())) + for o in tx.outputs(): + txOutput += int_to_hex(0 if o.asset else o.value.value, 8) + script = o.scriptpubkey.hex() + txOutput += var_int(len(script) // 2) + txOutput += script + txOutput = bfh(txOutput) + + for utxo in inputs: + + sequence = int_to_hex(utxo[5], 4) + + txtmp = bitcoinTransaction(bfh(utxo[0])) + trustedInput = btchip.getTrustedInput(cmd, txtmp, utxo[1]) + trustedInput['sequence'] = sequence + if segwitTransaction: + trustedInput['witness'] = True + chipInputs.append(trustedInput) + redeemScripts.append(txtmp.outputs[utxo[1]].script) + + # Sign all inputs + firstTransaction = True + inputIndex = 0 + rawTx = tx.serialize_to_network() + + btchip.enableAlternate2fa(cmd, False) + + while inputIndex < len(inputs): + btchip.startUntrustedTransaction(cmd, firstTransaction, inputIndex, + chipInputs, redeemScripts[inputIndex], version=tx.version) + # we don't set meaningful outputAddress, amount and fees + # as we only care about the alternateEncoding==True branch + outputData = btchip.finalizeInput(cmd, b'', 0, 0, changePath, bfh(rawTx)) + outputData['outputData'] = txOutput + if outputData['confirmationNeeded']: + outputData['address'] = output + else: + # Sign input with the provided PIN + inputSignature = btchip.untrustedHashSign(cmd, inputsPaths[inputIndex], pin, + lockTime=tx.locktime) + inputSignature[0] = 0x30 # force for 1.4.9+ + my_pubkey = inputs[inputIndex][4] + tx.add_signature_to_txin(txin_idx=inputIndex, + signing_pubkey=my_pubkey.hex(), + sig=inputSignature.hex()) + inputIndex = inputIndex + 1 + firstTransaction = False \ No newline at end of file diff --git a/tests/test_pubkey.py b/tests/test_pubkey.py index 351b709c..8fc62a64 100644 --- a/tests/test_pubkey.py +++ b/tests/test_pubkey.py @@ -26,7 +26,7 @@ def test_get_public_key(cmd): bip32_path=path, display=False ) - addrs.append((addr, base58_decode(addr)[1:21].hex())) + addrs.append((pub_key, addr, base58_decode(addr)[1:21].hex())) print("ADDRESSES:") print(addrs) \ No newline at end of file diff --git a/tests/test_sign.py b/tests/test_sign_asset_1_1.py similarity index 61% rename from tests/test_sign.py rename to tests/test_sign_asset_1_1.py index 094e5eb1..9844d981 100644 --- a/tests/test_sign.py +++ b/tests/test_sign_asset_1_1.py @@ -11,6 +11,8 @@ from bitcoin_client.hwi.serialization import CTransaction from bitcoin_client.exception import ConditionOfUseNotSatisfiedError from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction def sign_from_json(cmd, filepath: Path): tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) @@ -76,11 +78,45 @@ def sign_from_json(cmd, filepath: Path): # sign_from_json(cmd, filepath) -@automation("automations/accept.json") +#@automation("automations/accept.json") def test_sign_p2pkh_accept(cmd): #for filepath in Path("data").rglob("p2pkh/tx.json"): # sign_from_json(cmd, filepath) - sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('020000000115eb8abda69e314c60a693f1499871fe319587df46b24ab9c89b83e1abb6d7bf010000006b483045022100b776d19d402b062cae744374404cf29f586e94683b5c91183abdde4a2587315202205d68a66ffcb0f96d9aba0d2f3787b07b5f200f2dd87fc345598fa096f83128c8012103c2c6118e389d65e1b281bec87efc71aabb1fa485b63e7639037e442ec61ff5fbfeffffff02794beb56531000001976a914d9f6b08d5ec82b61360988d9619e6656d7b9b75c88ac4b5bbb91210200001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acd2ad1b00') + vin_prevout = transaction.TxOutpoint.from_str('69775d5e61078b15405dfd581713fa2dd4e92231b159e52d80246aead7708693:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin + ] + + vout = transaction.PartialTxOutput(value=10, scriptpubkey=bytes.fromhex('76a914c57f73045531ac70dc2c09a1da90fff59df5635588ac')) + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + + print(tx.is_complete()) + print(tx.serialize()) #@automation("automations/reject.json") #def test_sign_fail_p2pkh_reject(cmd): diff --git a/tests/test_sign_rvn_1_1.py b/tests/test_sign_rvn_1_1.py new file mode 100644 index 00000000..9844d981 --- /dev/null +++ b/tests/test_sign_rvn_1_1.py @@ -0,0 +1,124 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): +# # payloads do not matter, should check and fail before checking it (but non-empty is required) +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p1" +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p2" + + +#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): +# # should fail if the payload is less than 7 bytes +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") +# assert sw == 0x6700 + + +#@automation("automations/accept.json") +#def test_sign_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +#def test_sign_p2sh_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('020000000115eb8abda69e314c60a693f1499871fe319587df46b24ab9c89b83e1abb6d7bf010000006b483045022100b776d19d402b062cae744374404cf29f586e94683b5c91183abdde4a2587315202205d68a66ffcb0f96d9aba0d2f3787b07b5f200f2dd87fc345598fa096f83128c8012103c2c6118e389d65e1b281bec87efc71aabb1fa485b63e7639037e442ec61ff5fbfeffffff02794beb56531000001976a914d9f6b08d5ec82b61360988d9619e6656d7b9b75c88ac4b5bbb91210200001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acd2ad1b00') + vin_prevout = transaction.TxOutpoint.from_str('69775d5e61078b15405dfd581713fa2dd4e92231b159e52d80246aead7708693:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin + ] + + vout = transaction.PartialTxOutput(value=10, scriptpubkey=bytes.fromhex('76a914c57f73045531ac70dc2c09a1da90fff59df5635588ac')) + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + + print(tx.is_complete()) + print(tx.serialize()) + +#@automation("automations/reject.json") +#def test_sign_fail_p2pkh_reject(cmd): +# with pytest.raises(ConditionOfUseNotSatisfiedError): +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file From 3ec8d2afbca4fe8885120b5352c481bf30456484 Mon Sep 17 00:00:00 2001 From: kralverde Date: Fri, 25 Jun 2021 15:40:47 -0400 Subject: [PATCH 15/20] Tweaks --- include/btchip_helpers.h | 2 +- src/btchip_apdu_hash_input_finalize_full.c | 27 ++++- src/btchip_helpers.c | 4 +- src/main.c | 1 + tests/test_sign_asset_1_1.py | 8 +- tests/test_sign_asset_to_large_1_1.py | 121 ++++++++++++++++++++ tests/test_sign_asset_with_rvn_amount.py | 121 ++++++++++++++++++++ tests/test_sign_rvn_bad_script_1_1.py | 124 +++++++++++++++++++++ 8 files changed, 396 insertions(+), 12 deletions(-) create mode 100644 tests/test_sign_asset_to_large_1_1.py create mode 100644 tests/test_sign_asset_with_rvn_amount.py create mode 100644 tests/test_sign_rvn_bad_script_1_1.py diff --git a/include/btchip_helpers.h b/include/btchip_helpers.h index 195ebec2..02941397 100644 --- a/include/btchip_helpers.h +++ b/include/btchip_helpers.h @@ -42,7 +42,7 @@ unsigned char btchip_output_script_is_op_create(unsigned char *buffer, unsigned char btchip_output_script_is_op_call(unsigned char *buffer, size_t size); -unsigned char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned char *buffer); +signed char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned char *buffer); unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer, size_t size, int *ptr); void btchip_sleep16(unsigned short delay); diff --git a/src/btchip_apdu_hash_input_finalize_full.c b/src/btchip_apdu_hash_input_finalize_full.c index 49519b3b..b5a957b0 100644 --- a/src/btchip_apdu_hash_input_finalize_full.c +++ b/src/btchip_apdu_hash_input_finalize_full.c @@ -82,10 +82,25 @@ static bool check_output_displayable() { sizeof(btchip_context_D.currentOutput) - 8); isRavencoinAsset = nullAmount && - ((-1 != btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8)) || - (btchip_output_script_get_ravencoin_asset_ptr(btchip_context_D.currentOutput + 8, - sizeof(btchip_context_D.currentOutput) - 8, - &dummy))); + ((-1 != btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8)) + || + (btchip_output_script_get_ravencoin_asset_ptr( + btchip_context_D.currentOutput + 8, + sizeof(btchip_context_D.currentOutput) - 8, + &dummy + ))); + + if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { + PRINTF("Asset script value: %d\n", btchip_output_script_get_ravencoin_asset_ptr( + btchip_context_D.currentOutput + 8, + sizeof(btchip_context_D.currentOutput) - 8, + &dummy + )); + PRINTF("Null asset script value: %d\n", btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8)); + PRINTF("Null amount: %d\n", nullAmount); + PRINTF("isAsset: %d\n", isRavencoinAsset); + } + if (G_coin_config->kind == COIN_KIND_QTUM) { invalid_script = !btchip_output_script_is_regular(btchip_context_D.currentOutput + 8) && @@ -94,9 +109,9 @@ static bool check_output_displayable() { else if (nullAmount && G_coin_config->kind == COIN_KIND_RAVENCOIN) { // Ravencoin assets only come into play when there is a null amount invalid_script = - !isRavencoinAsset && + !isRavencoinAsset || ( !btchip_output_script_is_regular_ravencoin_asset(btchip_context_D.currentOutput + 8) && - !isP2sh && !isOpReturn; + !isP2sh && !isOpReturn); } else { invalid_script = diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index d9966aa0..c7c51abe 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -180,7 +180,7 @@ unsigned char btchip_output_script_is_op_call(unsigned char *buffer, return output_script_is_op_create_or_call(buffer, size, 0xC2); } -unsigned char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned char *buffer) { +signed char btchip_output_script_try_get_ravencoin_asset_tag_type(unsigned char *buffer) { if (btchip_output_script_is_regular(buffer) || btchip_output_script_is_p2sh(buffer) || btchip_output_script_is_op_return(buffer) || @@ -204,6 +204,7 @@ unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer unsigned char asset_len; if (final_op >= size || buffer[final_op] != 0x75) { + PRINTF("Ravencoin pointer quick return\n"); return 0; } while (script_ptr < final_op - 7) { // Definitely a bad asset script; too short @@ -259,6 +260,7 @@ unsigned char btchip_output_script_get_ravencoin_asset_ptr(unsigned char *buffer //There shouldn't be anything pushed larger than 256 bytes in an asset transfer script } } + PRINTF("Ravencoin pointer end\n"); return 0; } diff --git a/src/main.c b/src/main.c index 85b4b939..ebe06dac 100644 --- a/src/main.c +++ b/src/main.c @@ -985,6 +985,7 @@ uint8_t prepare_single_output() { } } else { + PRINTF("Normal out parsing\n"); str_len = strlen(G_coin_config->name_short); os_memmove(vars.tmp.fullAmount, G_coin_config->name_short, str_len); diff --git a/tests/test_sign_asset_1_1.py b/tests/test_sign_asset_1_1.py index 9844d981..17f54930 100644 --- a/tests/test_sign_asset_1_1.py +++ b/tests/test_sign_asset_1_1.py @@ -86,9 +86,9 @@ def test_sign_p2pkh_accept(cmd): tx = transaction.PartialTransaction() - in_tx = transaction.Transaction('020000000115eb8abda69e314c60a693f1499871fe319587df46b24ab9c89b83e1abb6d7bf010000006b483045022100b776d19d402b062cae744374404cf29f586e94683b5c91183abdde4a2587315202205d68a66ffcb0f96d9aba0d2f3787b07b5f200f2dd87fc345598fa096f83128c8012103c2c6118e389d65e1b281bec87efc71aabb1fa485b63e7639037e442ec61ff5fbfeffffff02794beb56531000001976a914d9f6b08d5ec82b61360988d9619e6656d7b9b75c88ac4b5bbb91210200001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acd2ad1b00') - vin_prevout = transaction.TxOutpoint.from_str('69775d5e61078b15405dfd581713fa2dd4e92231b159e52d80246aead7708693:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) + in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') + vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) vin.utxo = in_tx vin.script_type = 'p2pkh' vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] @@ -97,7 +97,7 @@ def test_sign_p2pkh_accept(cmd): vin ] - vout = transaction.PartialTxOutput(value=10, scriptpubkey=bytes.fromhex('76a914c57f73045531ac70dc2c09a1da90fff59df5635588ac')) + vout = transaction.PartialTxOutput(value=0, scriptpubkey=bytes.fromhex('76a9149730c97deaedf77d8d9c707a506bca4babadaf6988acc01572766e74085343414d434f494e00a3e1110000000075')) outputs = [ vout diff --git a/tests/test_sign_asset_to_large_1_1.py b/tests/test_sign_asset_to_large_1_1.py new file mode 100644 index 00000000..77cb2a59 --- /dev/null +++ b/tests/test_sign_asset_to_large_1_1.py @@ -0,0 +1,121 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): +# # payloads do not matter, should check and fail before checking it (but non-empty is required) +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p1" +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p2" + + +#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): +# # should fail if the payload is less than 7 bytes +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") +# assert sw == 0x6700 + + +#@automation("automations/accept.json") +#def test_sign_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +#def test_sign_p2sh_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') + vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin + ] + + vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvnt\x21SCAMCOINSCAMCOINSCAMCOINSCAMCOIN1\x00\xa3\xe1\x11\x00\x00\x00\x00u') + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + +#@automation("automations/reject.json") +#def test_sign_fail_p2pkh_reject(cmd): +# with pytest.raises(ConditionOfUseNotSatisfiedError): +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_asset_with_rvn_amount.py b/tests/test_sign_asset_with_rvn_amount.py new file mode 100644 index 00000000..71149dc6 --- /dev/null +++ b/tests/test_sign_asset_with_rvn_amount.py @@ -0,0 +1,121 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): +# # payloads do not matter, should check and fail before checking it (but non-empty is required) +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p1" +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p2" + + +#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): +# # should fail if the payload is less than 7 bytes +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") +# assert sw == 0x6700 + + +#@automation("automations/accept.json") +#def test_sign_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +#def test_sign_p2sh_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') + vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin + ] + + vout = transaction.PartialTxOutput(value=1, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvnt\x08SCAMCOIN\x00\xa3\xe1\x11\x00\x00\x00\x00u') + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + +#@automation("automations/reject.json") +#def test_sign_fail_p2pkh_reject(cmd): +# with pytest.raises(ConditionOfUseNotSatisfiedError): +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_rvn_bad_script_1_1.py b/tests/test_sign_rvn_bad_script_1_1.py new file mode 100644 index 00000000..705c7c30 --- /dev/null +++ b/tests/test_sign_rvn_bad_script_1_1.py @@ -0,0 +1,124 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): +# # payloads do not matter, should check and fail before checking it (but non-empty is required) +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p1" +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p2" + + +#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): +# # should fail if the payload is less than 7 bytes +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") +# assert sw == 0x6700 + + +#@automation("automations/accept.json") +#def test_sign_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +#def test_sign_p2sh_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('020000000115eb8abda69e314c60a693f1499871fe319587df46b24ab9c89b83e1abb6d7bf010000006b483045022100b776d19d402b062cae744374404cf29f586e94683b5c91183abdde4a2587315202205d68a66ffcb0f96d9aba0d2f3787b07b5f200f2dd87fc345598fa096f83128c8012103c2c6118e389d65e1b281bec87efc71aabb1fa485b63e7639037e442ec61ff5fbfeffffff02794beb56531000001976a914d9f6b08d5ec82b61360988d9619e6656d7b9b75c88ac4b5bbb91210200001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acd2ad1b00') + vin_prevout = transaction.TxOutpoint.from_str('69775d5e61078b15405dfd581713fa2dd4e92231b159e52d80246aead7708693:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin + ] + + vout = transaction.PartialTxOutput(value=10, scriptpubkey=bytes.fromhex('76a914c57f73045531ac70dc2c09a1da90fff59df563558811ac')) + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + + print(tx.is_complete()) + print(tx.serialize()) + +#@automation("automations/reject.json") +#def test_sign_fail_p2pkh_reject(cmd): +# with pytest.raises(ConditionOfUseNotSatisfiedError): +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file From 3975eceade07c1e742718077aeae1c780eedd704 Mon Sep 17 00:00:00 2001 From: kralverde Date: Fri, 25 Jun 2021 16:25:23 -0400 Subject: [PATCH 16/20] Tweaks --- src/main.c | 8 +- tests/test_sign_asset.py | 134 ++++++++++++++++++ tests/test_sign_asset_to_small_1_1.py | 121 ++++++++++++++++ ...d_script_1_1.py => test_sign_owner_1_1.py} | 8 +- tests/test_sign_owner_to_large_1_1.py | 121 ++++++++++++++++ 5 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 tests/test_sign_asset.py create mode 100644 tests/test_sign_asset_to_small_1_1.py rename tests/{test_sign_rvn_bad_script_1_1.py => test_sign_owner_1_1.py} (71%) create mode 100644 tests/test_sign_owner_to_large_1_1.py diff --git a/src/main.c b/src/main.c index ebe06dac..c248e92d 100644 --- a/src/main.c +++ b/src/main.c @@ -888,8 +888,14 @@ void get_address_from_output_script(unsigned char* script, int script_size, char unsigned short textSize; int addressOffset = 3; unsigned short version = G_coin_config->p2sh_version; + unsigned int dummy; - if (btchip_output_script_is_regular(script)) { + if (G_coin_config->kind == COIN_KIND_RAVENCOIN + && btchip_output_script_get_ravencoin_asset_ptr(script, script_size, &dummy) + && btchip_output_script_is_regular_ravencoin_asset(script)) { + addressOffset = 4; + version = G_coin_config->p2pkh_version; + } else if (btchip_output_script_is_regular(script)) { addressOffset = 4; version = G_coin_config->p2pkh_version; } diff --git a/tests/test_sign_asset.py b/tests/test_sign_asset.py new file mode 100644 index 00000000..f853f348 --- /dev/null +++ b/tests/test_sign_asset.py @@ -0,0 +1,134 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): +# # payloads do not matter, should check and fail before checking it (but non-empty is required) +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p1" +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p2" + + +#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): +# # should fail if the payload is less than 7 bytes +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") +# assert sw == 0x6700 + + +#@automation("automations/accept.json") +#def test_sign_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +#def test_sign_p2sh_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('0200000002905125de74ccf2772ad166bc7dec6115cd2293f25bebb2c22853a37a1859fb81000000006b483045022100fd3f1d4904a3131f23c427cf35badaa0747aa631e52a172b3fcfadf0268dabe9022034a5f4fd6ca75f9e54279f4b2775e3598db9997c8004a252d8a61e09ca0f091b01210351d088a9964f496a980f5e465c07c3647bf822562237f836cee3390520b261e9feffffff27366e81ca90bdfe13e07a878cadc5b8cfeb17af913dd6b04d6995b91e272262000000006b483045022100ab5c82658cb937e9a2630315e851c12937a3fded88702078f3013642c9796ceb02200c75cfd83839d09afd7accf6d116ca1c65ea0d2aaa366b205a5296868a6f4ea4012103fc2972ec144b6d72e4b8c03b007c2c7f02ac24ee41f5c5f7afe1a089e13ce8e3feffffff0206661500000000001976a91464c3646f741601535a6933367f30684ed7ab002788ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00e1f505000000007556ae1b00') + vin_prevout = transaction.TxOutpoint.from_str('bfefc88012690f1de0c75a972d3f0b6e3f7ad6e6dd9b95770d92f26d72c9ba8e:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00e1f5050000000075')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + in_tx2 = transaction.Transaction('0200000002f9b3284f2483378bbbe6547c990e235eb28c116c1668104d658497fc6ce7ec61000000006a473044022041a7a065faa2ba5a22723c5393d915abe6ab7ef51e6ec9a810c925cfa4dffbbd022027a0f9ca5220cf6ddc5295cbeb76b82254955099c63a4fe6ad4945356b501481012103486e130d517cc6f2b8e4fa435f7885ed1514ac0797cc37f906e58abd49ae3592feffffffc6663d94a04fa54d1d066abdcd5170ca4a180de44eb34e0942f33eb5a5ea73cd000000006a4730440220653a82aa1a46d4d3b0f101aab6bffdb1cc5ccff2d10e3ff08e701493a8677e640220019582b798b2611951d1a8c297eddde1e9aa5719706aa623cff9c7e86b0b2bb70121036e41ad9136a9dd1c2942fa63ad336c52a3c90839d3fd076e364aaf97c4748e57feffffff0269d88d00000000001976a9146d3f15868643bcf95224f0cc865b680386cfb8ef88ac00e1f505000000001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac59ae1b00') + vin_prevout = transaction.TxOutpoint.from_str('8dec0a2c4921ede211df3097f3ceed59d0ad099d353e728f5a39ca309d220440:1') + vin2 = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) + vin2.utxo = in_tx2 + vin2.script_type = 'p2pkh' + vin2.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin, + vin2 + ] + + vout = transaction.PartialTxOutput(value=0, scriptpubkey=bytes.fromhex('76a9146d3f15868643bcf95224f0cc865b680386cfb8ef88acc01572766e74085343414d434f494e00e1f5050000000075')) + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b', + b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0", + "44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + + print(tx.is_complete()) + print(tx.serialize()) + +#@automation("automations/reject.json") +#def test_sign_fail_p2pkh_reject(cmd): +# with pytest.raises(ConditionOfUseNotSatisfiedError): +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_asset_to_small_1_1.py b/tests/test_sign_asset_to_small_1_1.py new file mode 100644 index 00000000..3fd6055e --- /dev/null +++ b/tests/test_sign_asset_to_small_1_1.py @@ -0,0 +1,121 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): +# # payloads do not matter, should check and fail before checking it (but non-empty is required) +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p1" +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p2" + + +#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): +# # should fail if the payload is less than 7 bytes +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") +# assert sw == 0x6700 + + +#@automation("automations/accept.json") +#def test_sign_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +#def test_sign_p2sh_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') + vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin + ] + + vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvnt\x08SCAMCOIN\x00\xa3\xe1\x11\x00\x00\x00u') + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + +#@automation("automations/reject.json") +#def test_sign_fail_p2pkh_reject(cmd): +# with pytest.raises(ConditionOfUseNotSatisfiedError): +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_rvn_bad_script_1_1.py b/tests/test_sign_owner_1_1.py similarity index 71% rename from tests/test_sign_rvn_bad_script_1_1.py rename to tests/test_sign_owner_1_1.py index 705c7c30..864208c3 100644 --- a/tests/test_sign_rvn_bad_script_1_1.py +++ b/tests/test_sign_owner_1_1.py @@ -86,9 +86,9 @@ def test_sign_p2pkh_accept(cmd): tx = transaction.PartialTransaction() - in_tx = transaction.Transaction('020000000115eb8abda69e314c60a693f1499871fe319587df46b24ab9c89b83e1abb6d7bf010000006b483045022100b776d19d402b062cae744374404cf29f586e94683b5c91183abdde4a2587315202205d68a66ffcb0f96d9aba0d2f3787b07b5f200f2dd87fc345598fa096f83128c8012103c2c6118e389d65e1b281bec87efc71aabb1fa485b63e7639037e442ec61ff5fbfeffffff02794beb56531000001976a914d9f6b08d5ec82b61360988d9619e6656d7b9b75c88ac4b5bbb91210200001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acd2ad1b00') - vin_prevout = transaction.TxOutpoint.from_str('69775d5e61078b15405dfd581713fa2dd4e92231b159e52d80246aead7708693:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) + in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') + vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) vin.utxo = in_tx vin.script_type = 'p2pkh' vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] @@ -97,7 +97,7 @@ def test_sign_p2pkh_accept(cmd): vin ] - vout = transaction.PartialTxOutput(value=10, scriptpubkey=bytes.fromhex('76a914c57f73045531ac70dc2c09a1da90fff59df563558811ac')) + vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvno\x09SCAMCOIN!u') outputs = [ vout diff --git a/tests/test_sign_owner_to_large_1_1.py b/tests/test_sign_owner_to_large_1_1.py new file mode 100644 index 00000000..c5019320 --- /dev/null +++ b/tests/test_sign_owner_to_large_1_1.py @@ -0,0 +1,121 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation +from electrum_clone.ledger_sign_funcs import sign_transaction +from electrum_clone.electrumravencoin.electrum import transaction + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): +# # payloads do not matter, should check and fail before checking it (but non-empty is required) +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" +# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p1" +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") +# assert sw == 0x6B00, "should fail with non-zero p2" + + +#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): +# # should fail if the payload is less than 7 bytes +# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") +# assert sw == 0x6700 + + +#@automation("automations/accept.json") +#def test_sign_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +#def test_sign_p2sh_p2wpkh_accept(cmd): +# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): +# sign_from_json(cmd, filepath) + + +#@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + #for filepath in Path("data").rglob("p2pkh/tx.json"): + # sign_from_json(cmd, filepath) + #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + + tx = transaction.PartialTransaction() + + in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') + vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') + vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) + vin.utxo = in_tx + vin.script_type = 'p2pkh' + vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + + inputs = [ + vin + ] + + vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvno\x21SCAMCOINSCAMCOINSCAMCOINSCAMCOIN!u') + + outputs = [ + vout + ] + + tx._inputs = inputs + tx._outputs = outputs + + changePath = '' + + #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 + #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 + pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + inputsPaths = ["44'/175'/0'/0/0"] + + sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) + +#@automation("automations/reject.json") +#def test_sign_fail_p2pkh_reject(cmd): +# with pytest.raises(ConditionOfUseNotSatisfiedError): +# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file From d9af186b61e69cfcc8f954a6649ca8b5c79895af Mon Sep 17 00:00:00 2001 From: kralverde Date: Fri, 25 Jun 2021 21:41:08 -0400 Subject: [PATCH 17/20] Tests work! --- tests/electrum_clone/ledger_sign_funcs.py | 17 ++++++----------- tests/test_sign_asset.py | 12 +++++++----- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/tests/electrum_clone/ledger_sign_funcs.py b/tests/electrum_clone/ledger_sign_funcs.py index 7591ee61..6468ed10 100644 --- a/tests/electrum_clone/ledger_sign_funcs.py +++ b/tests/electrum_clone/ledger_sign_funcs.py @@ -13,20 +13,12 @@ def sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath): segwitTransaction = False pin = "" - print("SIGNING") - # Fetch inputs of the transaction to sign for i, txin in enumerate(tx.inputs()): redeemScript = Transaction.get_preimage_script(txin) + print("REDEEM SCRIPT: {}".format(redeemScript)) txin_prev_tx = txin.utxo txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None - print((txin_prev_tx_raw, - txin.prevout.out_idx, - redeemScript, - txin.prevout.txid.hex(), - pubkeys[i], - txin.nsequence, - txin.value_sats())) inputs.append([txin_prev_tx_raw, txin.prevout.out_idx, redeemScript, @@ -50,11 +42,13 @@ def sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath): txtmp = bitcoinTransaction(bfh(utxo[0])) trustedInput = btchip.getTrustedInput(cmd, txtmp, utxo[1]) trustedInput['sequence'] = sequence - if segwitTransaction: - trustedInput['witness'] = True + chipInputs.append(trustedInput) + print("REDEEM SCRIPT 2: {}".format(txtmp.outputs[utxo[1]].script)) redeemScripts.append(txtmp.outputs[utxo[1]].script) + print("INPUTS: {}".format(inputs)) + # Sign all inputs firstTransaction = True inputIndex = 0 @@ -63,6 +57,7 @@ def sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath): btchip.enableAlternate2fa(cmd, False) while inputIndex < len(inputs): + print('SIGNING: {}'.format(redeemScripts[inputIndex])) btchip.startUntrustedTransaction(cmd, firstTransaction, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees diff --git a/tests/test_sign_asset.py b/tests/test_sign_asset.py index f853f348..2ead716d 100644 --- a/tests/test_sign_asset.py +++ b/tests/test_sign_asset.py @@ -8,6 +8,7 @@ from ecdsa.keys import VerifyingKey from ecdsa.util import sigdecode_der +from bitcoin_client.bitcoin_utils import compress_pub_key from bitcoin_client.hwi.serialization import CTransaction from bitcoin_client.exception import ConditionOfUseNotSatisfiedError from utils import automation @@ -84,6 +85,8 @@ def test_sign_p2pkh_accept(cmd): # sign_from_json(cmd, filepath) #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') + comp = compress_pub_key(b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b') + tx = transaction.PartialTransaction() in_tx = transaction.Transaction('0200000002905125de74ccf2772ad166bc7dec6115cd2293f25bebb2c22853a37a1859fb81000000006b483045022100fd3f1d4904a3131f23c427cf35badaa0747aa631e52a172b3fcfadf0268dabe9022034a5f4fd6ca75f9e54279f4b2775e3598db9997c8004a252d8a61e09ca0f091b01210351d088a9964f496a980f5e465c07c3647bf822562237f836cee3390520b261e9feffffff27366e81ca90bdfe13e07a878cadc5b8cfeb17af913dd6b04d6995b91e272262000000006b483045022100ab5c82658cb937e9a2630315e851c12937a3fded88702078f3013642c9796ceb02200c75cfd83839d09afd7accf6d116ca1c65ea0d2aaa366b205a5296868a6f4ea4012103fc2972ec144b6d72e4b8c03b007c2c7f02ac24ee41f5c5f7afe1a089e13ce8e3feffffff0206661500000000001976a91464c3646f741601535a6933367f30684ed7ab002788ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00e1f505000000007556ae1b00') @@ -91,21 +94,21 @@ def test_sign_p2pkh_accept(cmd): vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00e1f5050000000075')) vin.utxo = in_tx vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + vin.pubkeys = [comp] in_tx2 = transaction.Transaction('0200000002f9b3284f2483378bbbe6547c990e235eb28c116c1668104d658497fc6ce7ec61000000006a473044022041a7a065faa2ba5a22723c5393d915abe6ab7ef51e6ec9a810c925cfa4dffbbd022027a0f9ca5220cf6ddc5295cbeb76b82254955099c63a4fe6ad4945356b501481012103486e130d517cc6f2b8e4fa435f7885ed1514ac0797cc37f906e58abd49ae3592feffffffc6663d94a04fa54d1d066abdcd5170ca4a180de44eb34e0942f33eb5a5ea73cd000000006a4730440220653a82aa1a46d4d3b0f101aab6bffdb1cc5ccff2d10e3ff08e701493a8677e640220019582b798b2611951d1a8c297eddde1e9aa5719706aa623cff9c7e86b0b2bb70121036e41ad9136a9dd1c2942fa63ad336c52a3c90839d3fd076e364aaf97c4748e57feffffff0269d88d00000000001976a9146d3f15868643bcf95224f0cc865b680386cfb8ef88ac00e1f505000000001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac59ae1b00') vin_prevout = transaction.TxOutpoint.from_str('8dec0a2c4921ede211df3097f3ceed59d0ad099d353e728f5a39ca309d220440:1') vin2 = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) vin2.utxo = in_tx2 vin2.script_type = 'p2pkh' - vin2.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + vin2.pubkeys = [comp] inputs = [ vin, vin2 ] - vout = transaction.PartialTxOutput(value=0, scriptpubkey=bytes.fromhex('76a9146d3f15868643bcf95224f0cc865b680386cfb8ef88acc01572766e74085343414d434f494e00e1f5050000000075')) + vout = transaction.PartialTxOutput(asset='SCAMCOIN', value=1, scriptpubkey=bytes.fromhex('76a9146d3f15868643bcf95224f0cc865b680386cfb8ef88acc01572766e74085343414d434f494e00e1f5050000000075')) outputs = [ vout @@ -118,8 +121,7 @@ def test_sign_p2pkh_accept(cmd): #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b', - b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] + pubkeys = [comp, comp] inputsPaths = ["44'/175'/0'/0/0", "44'/175'/0'/0/0"] From 5969ae5587434aa1236d893678000fca08f60c3e Mon Sep 17 00:00:00 2001 From: kralverde Date: Fri, 25 Jun 2021 21:50:06 -0400 Subject: [PATCH 18/20] reversions --- ledger-nanos-sdk | 1 - tests/.gitignore | 1 - tests/bitcoin_client/bitcoin_cmd.py | 6 +- tests/btchippython/.gitignore | 2 - tests/btchippython/.gitmodules | 0 tests/btchippython/LICENSE | 202 ----- tests/btchippython/MANIFEST.in | 3 - tests/btchippython/README.md | 37 - tests/btchippython/btchip/__init__.py | 20 - .../btchippython/btchip/bitcoinTransaction.py | 165 ---- tests/btchippython/btchip/bitcoinVarint.py | 63 -- tests/btchippython/btchip/btchip.py | 712 ------------------ tests/btchippython/btchip/btchipComm.py | 254 ------- tests/btchippython/btchip/btchipException.py | 28 - .../btchip/btchipFirmwareWizard.py | 24 - tests/btchippython/btchip/btchipHelpers.py | 86 --- .../btchippython/btchip/btchipKeyRecovery.py | 57 -- .../btchippython/btchip/btchipPersoWizard.py | 376 --------- tests/btchippython/btchip/btchipUtils.py | 105 --- tests/btchippython/btchip/ledgerWrapper.py | 92 --- tests/btchippython/btchip/msqr.py | 94 --- tests/btchippython/btchip/ui/__init__.py | 19 - .../btchip/ui/personalization00start.py | 55 -- .../btchip/ui/personalization01seed.py | 77 -- .../btchip/ui/personalization02security.py | 96 --- .../btchip/ui/personalization03config.py | 68 -- .../btchip/ui/personalization04finalize.py | 63 -- .../btchip/ui/personalizationseedbackup01.py | 71 -- .../btchip/ui/personalizationseedbackup02.py | 46 -- .../btchip/ui/personalizationseedbackup03.py | 76 -- .../btchip/ui/personalizationseedbackup04.py | 50 -- .../samples/getFirmwareVersion.py | 26 - tests/btchippython/samples/runScript.py | 54 -- tests/btchippython/setup.py | 31 - .../coinkite-cosigning/testGetPublicKeyDev.js | 115 --- .../tests/coinkite-cosigning/testSignDev.js | 168 ----- tests/btchippython/tests/testConnectivity.py | 34 - .../tests/testMessageSignature.py | 56 -- .../btchippython/tests/testMultisigArmory.py | 194 ----- .../tests/testMultisigArmoryNo2FA.py | 169 ----- .../tests/testSimpleTransaction.py | 78 -- tests/btchippython/ui/make.sh | 12 - .../ui/personalization-00-start.ui | 98 --- .../ui/personalization-01-seed.ui | 160 ---- .../ui/personalization-02-security.ui | 226 ------ .../ui/personalization-03-config.ui | 136 ---- .../ui/personalization-04-finalize.ui | 122 --- .../ui/personalization-seedbackup-01.ui | 138 ---- .../ui/personalization-seedbackup-02.ui | 69 -- .../ui/personalization-seedbackup-03.ui | 165 ---- .../ui/personalization-seedbackup-04.ui | 82 -- tests/clean_tests.sh | 2 +- tests/conftest.py | 6 +- tests/data/many-to-many/p2pkh/tx.json | 4 +- tests/data/one-to-one/p2pkh/tx.json | 20 +- tests/electrum_clone/electrumravencoin | 1 - tests/electrum_clone/ledger_sign_funcs.py | 79 -- tests/prepare_tests.sh | 4 +- tests/test_get_coin_version.py | 13 +- tests/test_get_firmware_version.py | 5 +- tests/test_get_pubkey.py | 42 ++ tests/test_get_random.py | 15 + tests/test_get_trusted_inputs.py | 20 +- tests/test_pubkey.py | 32 - tests/test_sign.py | 89 +++ tests/test_sign_asset.py | 136 ---- tests/test_sign_asset_1_1.py | 124 --- tests/test_sign_asset_to_large_1_1.py | 121 --- tests/test_sign_asset_to_small_1_1.py | 121 --- tests/test_sign_asset_with_rvn_amount.py | 121 --- tests/test_sign_message.py | 109 --- tests/test_sign_owner_1_1.py | 124 --- tests/test_sign_owner_to_large_1_1.py | 121 --- tests/test_sign_rvn_1_1.py | 124 --- tests/test_verify.py | 22 - 75 files changed, 186 insertions(+), 6351 deletions(-) delete mode 160000 ledger-nanos-sdk delete mode 100644 tests/btchippython/.gitignore delete mode 100644 tests/btchippython/.gitmodules delete mode 100644 tests/btchippython/LICENSE delete mode 100644 tests/btchippython/MANIFEST.in delete mode 100644 tests/btchippython/README.md delete mode 100644 tests/btchippython/btchip/__init__.py delete mode 100644 tests/btchippython/btchip/bitcoinTransaction.py delete mode 100644 tests/btchippython/btchip/bitcoinVarint.py delete mode 100644 tests/btchippython/btchip/btchip.py delete mode 100644 tests/btchippython/btchip/btchipComm.py delete mode 100644 tests/btchippython/btchip/btchipException.py delete mode 100644 tests/btchippython/btchip/btchipFirmwareWizard.py delete mode 100644 tests/btchippython/btchip/btchipHelpers.py delete mode 100644 tests/btchippython/btchip/btchipKeyRecovery.py delete mode 100644 tests/btchippython/btchip/btchipPersoWizard.py delete mode 100644 tests/btchippython/btchip/btchipUtils.py delete mode 100644 tests/btchippython/btchip/ledgerWrapper.py delete mode 100644 tests/btchippython/btchip/msqr.py delete mode 100644 tests/btchippython/btchip/ui/__init__.py delete mode 100644 tests/btchippython/btchip/ui/personalization00start.py delete mode 100644 tests/btchippython/btchip/ui/personalization01seed.py delete mode 100644 tests/btchippython/btchip/ui/personalization02security.py delete mode 100644 tests/btchippython/btchip/ui/personalization03config.py delete mode 100644 tests/btchippython/btchip/ui/personalization04finalize.py delete mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup01.py delete mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup02.py delete mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup03.py delete mode 100644 tests/btchippython/btchip/ui/personalizationseedbackup04.py delete mode 100644 tests/btchippython/samples/getFirmwareVersion.py delete mode 100644 tests/btchippython/samples/runScript.py delete mode 100644 tests/btchippython/setup.py delete mode 100644 tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js delete mode 100644 tests/btchippython/tests/coinkite-cosigning/testSignDev.js delete mode 100644 tests/btchippython/tests/testConnectivity.py delete mode 100644 tests/btchippython/tests/testMessageSignature.py delete mode 100644 tests/btchippython/tests/testMultisigArmory.py delete mode 100644 tests/btchippython/tests/testMultisigArmoryNo2FA.py delete mode 100644 tests/btchippython/tests/testSimpleTransaction.py delete mode 100755 tests/btchippython/ui/make.sh delete mode 100644 tests/btchippython/ui/personalization-00-start.ui delete mode 100644 tests/btchippython/ui/personalization-01-seed.ui delete mode 100644 tests/btchippython/ui/personalization-02-security.ui delete mode 100644 tests/btchippython/ui/personalization-03-config.ui delete mode 100644 tests/btchippython/ui/personalization-04-finalize.ui delete mode 100644 tests/btchippython/ui/personalization-seedbackup-01.ui delete mode 100644 tests/btchippython/ui/personalization-seedbackup-02.ui delete mode 100644 tests/btchippython/ui/personalization-seedbackup-03.ui delete mode 100644 tests/btchippython/ui/personalization-seedbackup-04.ui delete mode 160000 tests/electrum_clone/electrumravencoin delete mode 100644 tests/electrum_clone/ledger_sign_funcs.py create mode 100644 tests/test_get_pubkey.py create mode 100644 tests/test_get_random.py delete mode 100644 tests/test_pubkey.py create mode 100644 tests/test_sign.py delete mode 100644 tests/test_sign_asset.py delete mode 100644 tests/test_sign_asset_1_1.py delete mode 100644 tests/test_sign_asset_to_large_1_1.py delete mode 100644 tests/test_sign_asset_to_small_1_1.py delete mode 100644 tests/test_sign_asset_with_rvn_amount.py delete mode 100644 tests/test_sign_message.py delete mode 100644 tests/test_sign_owner_1_1.py delete mode 100644 tests/test_sign_owner_to_large_1_1.py delete mode 100644 tests/test_sign_rvn_1_1.py delete mode 100644 tests/test_verify.py diff --git a/ledger-nanos-sdk b/ledger-nanos-sdk deleted file mode 160000 index 30655d1b..00000000 --- a/ledger-nanos-sdk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 30655d1b2aea4dbcf731e2697208d9fdcaa39782 diff --git a/tests/.gitignore b/tests/.gitignore index a545cd8c..3936d581 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,2 @@ bitcoin-bin bitcoin-testnet-bin -ravencoin-bin diff --git a/tests/bitcoin_client/bitcoin_cmd.py b/tests/bitcoin_client/bitcoin_cmd.py index c32fc37d..ec5818fd 100644 --- a/tests/bitcoin_client/bitcoin_cmd.py +++ b/tests/bitcoin_client/bitcoin_cmd.py @@ -75,7 +75,7 @@ def sign_new_tx(self, sign_pub_keys: List[bytes] = [] for sign_path in sign_paths: sign_pub_key, _, _ = self.get_public_key( - addr_type=AddrType.Legacy, + addr_type=AddrType.BECH32, bip32_path=sign_path, display=False ) @@ -122,7 +122,7 @@ def sign_new_tx(self, if amount_available - fees > amount: change_pub_key, _, _ = self.get_public_key( - addr_type=AddrType.Legacy, + addr_type=AddrType.BECH32, bip32_path=change_path, display=False ) @@ -169,7 +169,7 @@ def sign_new_tx(self, base58_decode(address)[1:-4] + # hash160(redeem_script) b"\x87") # OP_EQUAL # P2PKH address (mainnet and testnet) - elif address.startswith("R") or (address.startswith("m") or address.startswith("n")): + elif address.startswith("1") or (address.startswith("m") or address.startswith("n")): script_pub_key = (b"\x76" + # OP_DUP b"\xa9" + # OP_HASH160 b"\x14" + # bytes to push (20) diff --git a/tests/btchippython/.gitignore b/tests/btchippython/.gitignore deleted file mode 100644 index 2f78cf5b..00000000 --- a/tests/btchippython/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pyc - diff --git a/tests/btchippython/.gitmodules b/tests/btchippython/.gitmodules deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/btchippython/LICENSE b/tests/btchippython/LICENSE deleted file mode 100644 index d6456956..00000000 --- a/tests/btchippython/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/tests/btchippython/MANIFEST.in b/tests/btchippython/MANIFEST.in deleted file mode 100644 index 3eb1cb4b..00000000 --- a/tests/btchippython/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include README.md -include LICENSE - diff --git a/tests/btchippython/README.md b/tests/btchippython/README.md deleted file mode 100644 index 3d71f575..00000000 --- a/tests/btchippython/README.md +++ /dev/null @@ -1,37 +0,0 @@ -btchip-python -============= - -Python communication library for Ledger Hardware Wallet products - -Requirements -------------- - -This API is available on pip - install with pip install btchip-python - -Building on a Unix platform requires libusb-1.0-0-dev and libudev-dev installed previously - -Interim Debian packages have also been built by Richard Ulrich at https://launchpad.net/~richi-paraeasy/+archive/ubuntu/bitcoin/ (btchip-python, hidapi and python-hidapi) - -For optional BIP 39 support during dongle setup, also install https://github.com/trezor/python-mnemonic - also available as a Debian package at the previous link (python-mnemonic) - -Building on Windows --------------------- - - - Download and install the latest Python 2.7 version from https://www.python.org/downloads/windows/ - - Install Microsoft Visual C++ Compiler for Python 2.7 from http://www.microsoft.com/en-us/download/details.aspx?id=44266 - - Download and install PyQt4 for Python 2.7 from https://www.riverbankcomputing.com/software/pyqt/download - - Install the btchip library (open a command prompt and enter c:\python27\scripts\pip install btchip) - -Building/Installing on FreeBSD ------------------------------- - -On FreeBSD you can install the packages: - - pkg install security/py-btchip-python - -or build via ports: - - cd /usr/ports/security/py-btchip-python - make install clean - - diff --git a/tests/btchippython/btchip/__init__.py b/tests/btchippython/btchip/__init__.py deleted file mode 100644 index 9e0789e1..00000000 --- a/tests/btchippython/btchip/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" -__version__ = "0.1.32" - diff --git a/tests/btchippython/btchip/bitcoinTransaction.py b/tests/btchippython/btchip/bitcoinTransaction.py deleted file mode 100644 index 35276e9e..00000000 --- a/tests/btchippython/btchip/bitcoinTransaction.py +++ /dev/null @@ -1,165 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from .bitcoinVarint import * -from binascii import hexlify - -class bitcoinInput: - - def __init__(self, bufferOffset=None): - self.prevOut = "" - self.script = "" - self.sequence = "" - if bufferOffset is not None: - buf = bufferOffset['buffer'] - offset = bufferOffset['offset'] - self.prevOut = buf[offset:offset + 36] - offset += 36 - scriptSize = readVarint(buf, offset) - offset += scriptSize['size'] - self.script = buf[offset:offset + scriptSize['value']] - offset += scriptSize['value'] - self.sequence = buf[offset:offset + 4] - offset += 4 - bufferOffset['offset'] = offset - - def serialize(self): - result = [] - result.extend(self.prevOut) - writeVarint(len(self.script), result) - result.extend(self.script) - result.extend(self.sequence) - return result - - def __str__(self): - buf = "Prevout : " + hexlify(self.prevOut) + "\r\n" - buf += "Script : " + hexlify(self.script) + "\r\n" - buf += "Sequence : " + hexlify(self.sequence) + "\r\n" - return buf - -class bitcoinOutput: - - def __init__(self, bufferOffset=None): - self.amount = "" - self.script = "" - if bufferOffset is not None: - buf = bufferOffset['buffer'] - offset = bufferOffset['offset'] - self.amount = buf[offset:offset + 8] - offset += 8 - scriptSize = readVarint(buf, offset) - offset += scriptSize['size'] - self.script = buf[offset:offset + scriptSize['value']] - offset += scriptSize['value'] - bufferOffset['offset'] = offset - - def serialize(self): - result = [] - result.extend(self.amount) - writeVarint(len(self.script), result) - result.extend(self.script) - return result - - def __str__(self): - buf = "Amount : " + hexlify(self.amount) + "\r\n" - buf += "Script : " + hexlify(self.script) + "\r\n" - return buf - - -class bitcoinTransaction: - - def __init__(self, data=None): - self.version = "" - self.inputs = [] - self.outputs = [] - self.lockTime = "" - self.witness = False - self.witnessScript = "" - if data is not None: - offset = 0 - self.version = data[offset:offset + 4] - offset += 4 - if (data[offset] == 0) and (data[offset + 1] != 0): - offset += 2 - self.witness = True - inputSize = readVarint(data, offset) - offset += inputSize['size'] - numInputs = inputSize['value'] - for i in range(numInputs): - tmp = { 'buffer': data, 'offset' : offset} - self.inputs.append(bitcoinInput(tmp)) - offset = tmp['offset'] - outputSize = readVarint(data, offset) - offset += outputSize['size'] - numOutputs = outputSize['value'] - for i in range(numOutputs): - tmp = { 'buffer': data, 'offset' : offset} - self.outputs.append(bitcoinOutput(tmp)) - offset = tmp['offset'] - if self.witness: - self.witnessScript = data[offset : len(data) - 4] - self.lockTime = data[len(data) - 4:] - else: - self.lockTime = data[offset:offset + 4] - - def serialize(self, skipOutputLocktime=False, skipWitness=False): - if skipWitness or (not self.witness): - useWitness = False - else: - useWitness = True - result = [] - result.extend(self.version) - if useWitness: - result.append(0x00) - result.append(0x01) - writeVarint(len(self.inputs), result) - for trinput in self.inputs: - result.extend(trinput.serialize()) - if not skipOutputLocktime: - writeVarint(len(self.outputs), result) - for troutput in self.outputs: - result.extend(troutput.serialize()) - if useWitness: - result.extend(self.witnessScript) - result.extend(self.lockTime) - return result - - def serializeOutputs(self): - result = [] - writeVarint(len(self.outputs), result) - for troutput in self.outputs: - result.extend(troutput.serialize()) - return result - - def __str__(self): - buf = "Version : " + hexlify(self.version) + "\r\n" - index = 1 - for trinput in self.inputs: - buf += "Input #" + str(index) + "\r\n" - buf += str(trinput) - index+=1 - index = 1 - for troutput in self.outputs: - buf += "Output #" + str(index) + "\r\n" - buf += str(troutput) - index+=1 - buf += "Locktime : " + hexlify(self.lockTime) + "\r\n" - if self.witness: - buf += "Witness script : " + hexlify(self.witnessScript) + "\r\n" - return buf diff --git a/tests/btchippython/btchip/bitcoinVarint.py b/tests/btchippython/btchip/bitcoinVarint.py deleted file mode 100644 index a0dd8680..00000000 --- a/tests/btchippython/btchip/bitcoinVarint.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from .btchipException import BTChipException - -def readVarint(buffer, offset): - varintSize = 0 - value = 0 - if (buffer[offset] < 0xfd): - value = buffer[offset] - varintSize = 1 - elif (buffer[offset] == 0xfd): - value = (buffer[offset + 2] << 8) | (buffer[offset + 1]) - varintSize = 3 - elif (buffer[offset] == 0xfe): - value = (buffer[offset + 4] << 24) | (buffer[offset + 3] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 1]) - varintSize = 5 - else: - raise BTChipException("unsupported varint") - return { "value": value, "size": varintSize } - -def writeVarint(value, buffer): - if (value < 0xfd): - buffer.append(value) - elif (value <= 0xffff): - buffer.append(0xfd) - buffer.append(value & 0xff) - buffer.append((value >> 8) & 0xff) - elif (value <= 0xffffffff): - buffer.append(0xfe) - buffer.append(value & 0xff) - buffer.append((value >> 8) & 0xff) - buffer.append((value >> 16) & 0xff) - buffer.append((value >> 24) & 0xff) - else: - raise BTChipException("unsupported encoding") - return buffer - -def getVarintSize(value): - if (value < 0xfd): - return 1 - elif (value <= 0xffff): - return 3 - elif (value <= 0xffffffff): - return 5 - else: - raise BTChipException("unsupported encoding") diff --git a/tests/btchippython/btchip/btchip.py b/tests/btchippython/btchip/btchip.py deleted file mode 100644 index 11e411fb..00000000 --- a/tests/btchippython/btchip/btchip.py +++ /dev/null @@ -1,712 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from .btchipComm import * -from .bitcoinTransaction import * -from .bitcoinVarint import * -from .btchipException import * -from .btchipHelpers import * -from .btchipKeyRecovery import * -from binascii import hexlify, unhexlify - -class btchip: - BTCHIP_CLA = 0xe0 - BTCHIP_JC_EXT_CLA = 0xf0 - - BTCHIP_INS_SET_ALTERNATE_COIN_VERSION = 0x14 - BTCHIP_INS_SETUP = 0x20 - BTCHIP_INS_VERIFY_PIN = 0x22 - BTCHIP_INS_GET_OPERATION_MODE = 0x24 - BTCHIP_INS_SET_OPERATION_MODE = 0x26 - BTCHIP_INS_SET_KEYMAP = 0x28 - BTCHIP_INS_SET_COMM_PROTOCOL = 0x2a - BTCHIP_INS_GET_WALLET_PUBLIC_KEY = 0x40 - BTCHIP_INS_GET_TRUSTED_INPUT = 0x42 - BTCHIP_INS_HASH_INPUT_START = 0x44 - BTCHIP_INS_HASH_INPUT_FINALIZE = 0x46 - BTCHIP_INS_HASH_SIGN = 0x48 - BTCHIP_INS_HASH_INPUT_FINALIZE_FULL = 0x4a - BTCHIP_INS_GET_INTERNAL_CHAIN_INDEX = 0x4c - BTCHIP_INS_SIGN_MESSAGE = 0x4e - BTCHIP_INS_GET_TRANSACTION_LIMIT = 0xa0 - BTCHIP_INS_SET_TRANSACTION_LIMIT = 0xa2 - BTCHIP_INS_IMPORT_PRIVATE_KEY = 0xb0 - BTCHIP_INS_GET_PUBLIC_KEY = 0xb2 - BTCHIP_INS_DERIVE_BIP32_KEY = 0xb4 - BTCHIP_INS_SIGNVERIFY_IMMEDIATE = 0xb6 - BTCHIP_INS_GET_RANDOM = 0xc0 - BTCHIP_INS_GET_ATTESTATION = 0xc2 - BTCHIP_INS_GET_FIRMWARE_VERSION = 0xc4 - BTCHIP_INS_COMPOSE_MOFN_ADDRESS = 0xc6 - BTCHIP_INS_GET_POS_SEED = 0xca - - BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY = 0x20 - BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY = 0x22 - BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY = 0x24 - BTCHIP_INS_EXT_CACHE_GET_FEATURES = 0x26 - - OPERATION_MODE_WALLET = 0x01 - OPERATION_MODE_RELAXED_WALLET = 0x02 - OPERATION_MODE_SERVER = 0x04 - OPERATION_MODE_DEVELOPER = 0x08 - - FEATURE_UNCOMPRESSED_KEYS = 0x01 - FEATURE_RFC6979 = 0x02 - FEATURE_FREE_SIGHASHTYPE = 0x04 - FEATURE_NO_2FA_P2SH = 0x08 - - QWERTY_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f313035")) - QWERTZ_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f313035")) - AZERTY_KEYMAP = bytearray(unhexlify("08000000010000200100007820c8ffc3feffff07000000002c38202030341e21222d352e102e3637271e1f202122232425263736362e37101f1405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f64302f2d351405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f643035")) - - def __init__(self, dongle): - self.dongle = dongle - self.needKeyCache = False - try: - firmware = self.getFirmwareVersion()['version'] - self.multiOutputSupported = tuple(map(int, (firmware.split(".")))) >= (1, 1, 4) - if self.multiOutputSupported: - self.scriptBlockLength = 50 - else: - self.scriptBlockLength = 255 - except Exception: - pass - try: - result = self.getJCExtendedFeatures() - self.needKeyCache = (result['proprietaryApi'] == False) - except Exception: - pass - - def setAlternateCoinVersion(self, versionRegular, versionP2SH): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_ALTERNATE_COIN_VERSION, 0x00, 0x00, 0x02, versionRegular, versionP2SH] - self.dongle.exchange(bytearray(apdu)) - - def verifyPin(self, pin): - if isinstance(pin, str): - pin = pin.encode('utf-8') - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x00, 0x00, len(pin) ] - apdu.extend(bytearray(pin)) - self.dongle.exchange(bytearray(apdu)) - - def getVerifyPinRemainingAttempts(self): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x80, 0x00, 0x01 ] - apdu.extend(bytearray(b'0')) - try: - self.dongle.exchange(bytearray(apdu)) - except BTChipException as e: - if ((e.sw & 0xfff0) == 0x63c0): - return e.sw - 0x63c0 - raise e - - def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False): - result = {} - donglePath = parse_bip32_path(path) - if self.needKeyCache: - self.resolvePublicKeysInPath(path) - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_WALLET_PUBLIC_KEY, 0x01 if showOnScreen else 0x00, 0x03 if cashAddr else 0x02 if segwitNative else 0x01 if segwit else 0x00, len(donglePath) ] - apdu.extend(donglePath) - response = self.dongle.exchange(bytearray(apdu)) - offset = 0 - result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] - offset = offset + 1 + response[offset] - result['address'] = str(response[offset + 1 : offset + 1 + response[offset]]) - offset = offset + 1 + response[offset] - result['chainCode'] = response[offset : offset + 32] - return result - - @classmethod - def getTrustedInput(self, cmd, transaction, index): - result = {} - # Header - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x00, 0x00 ] - params = bytearray.fromhex("%.8x" % (index)) - params.extend(transaction.version) - writeVarint(len(transaction.inputs), params) - apdu.append(len(params)) - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - # Each input - for trinput in transaction.inputs: - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] - params = bytearray(trinput.prevOut) - writeVarint(len(trinput.script), params) - apdu.append(len(params)) - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - offset = 0 - while True: - blockLength = 251 - if ((offset + blockLength) < len(trinput.script)): - dataLength = blockLength - else: - dataLength = len(trinput.script) - offset - params = bytearray(trinput.script[offset : offset + dataLength]) - if ((offset + dataLength) == len(trinput.script)): - params.extend(trinput.sequence) - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(params) ] - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - offset += dataLength - if (offset >= len(trinput.script)): - break - # Number of outputs - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] - params = [] - writeVarint(len(transaction.outputs), params) - apdu.append(len(params)) - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - # Each output - indexOutput = 0 - for troutput in transaction.outputs: - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] - params = bytearray(troutput.amount) - writeVarint(len(troutput.script), params) - apdu.append(len(params)) - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - offset = 0 - while (offset < len(troutput.script)): - blockLength = 255 - if ((offset + blockLength) < len(troutput.script)): - dataLength = blockLength - else: - dataLength = len(troutput.script) - offset - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, dataLength ] - apdu.extend(troutput.script[offset : offset + dataLength]) - cmd.transport.exchange_raw(bytearray(apdu)) - offset += dataLength - # Locktime - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(transaction.lockTime) ] - apdu.extend(transaction.lockTime) - _, response = cmd.transport.exchange_raw(bytearray(apdu)) - result['trustedInput'] = True - result['value'] = response - return result - - @classmethod - def startUntrustedTransaction(self, cmd, newTransaction, inputIndex, outputList, redeemScript, version=0x01, cashAddr=False, continueSegwit=False): - # Start building a fake transaction with the passed inputs - segwit = False - if newTransaction: - for passedOutput in outputList: - if ('witness' in passedOutput) and passedOutput['witness']: - segwit = True - break - if newTransaction: - if segwit: - p2 = 0x03 if cashAddr else 0x02 - else: - p2 = 0x00 - else: - p2 = 0x10 if continueSegwit else 0x80 - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x00, p2 ] - params = bytearray([version, 0x00, 0x00, 0x00]) - writeVarint(len(outputList), params) - apdu.append(len(params)) - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - # Loop for each input - currentIndex = 0 - for passedOutput in outputList: - if ('sequence' in passedOutput) and passedOutput['sequence']: - sequence = bytearray(unhexlify(passedOutput['sequence'])) - else: - sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) # default sequence - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00 ] - params = [] - script = bytearray(redeemScript) - if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: - params.append(0x01) - elif ('witness' in passedOutput) and passedOutput['witness']: - params.append(0x02) - else: - params.append(0x00) - if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: - params.append(len(passedOutput['value'])) - params.extend(passedOutput['value']) - if currentIndex != inputIndex: - script = bytearray() - writeVarint(len(script), params) - apdu.append(len(params)) - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - offset = 0 - while(offset < len(script)): - blockLength = 255 - if ((offset + blockLength) < len(script)): - dataLength = blockLength - else: - dataLength = len(script) - offset - params = script[offset : offset + dataLength] - if ((offset + dataLength) == len(script)): - params.extend(sequence) - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(params) ] - apdu.extend(params) - cmd.transport.exchange_raw(bytearray(apdu)) - offset += blockLength - if len(script) == 0: - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(sequence) ] - apdu.extend(sequence) - cmd.transport.exchange_raw(bytearray(apdu)) - currentIndex += 1 - - @classmethod - def finalizeInput(self, cmd, outputAddress, amount, fees, changePath, rawTx=None): - alternateEncoding = False - donglePath = parse_bip32_path(changePath) - result = {} - outputs = None - if rawTx is not None: - try: - fullTx = bitcoinTransaction(bytearray(rawTx)) - outputs = fullTx.serializeOutputs() - if len(donglePath) != 0: - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, 0xFF, 0x00 ] - params = [] - params.extend(donglePath) - apdu.append(len(params)) - apdu.extend(params) - _, response = cmd.transport.exchange_raw(bytearray(apdu)) - offset = 0 - while (offset < len(outputs)): - blockLength = 50 - if ((offset + blockLength) < len(outputs)): - dataLength = blockLength - p1 = 0x00 - else: - dataLength = len(outputs) - offset - p1 = 0x80 - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ - p1, 0x00, dataLength ] - apdu.extend(outputs[offset : offset + dataLength]) - _, response = cmd.transport.exchange_raw(bytearray(apdu)) - offset += dataLength - alternateEncoding = True - except Exception: - pass - if not alternateEncoding: - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ] - params = [] - params.append(len(outputAddress)) - params.extend(bytearray(outputAddress)) - writeHexAmountBE(btc_to_satoshi(str(amount)), params) - writeHexAmountBE(btc_to_satoshi(str(fees)), params) - params.extend(donglePath) - apdu.append(len(params)) - apdu.extend(params) - _, response = cmd.transport.exchange_raw(bytearray(apdu)) - result['confirmationNeeded'] = response[1 + response[0]] != 0x00 - result['confirmationType'] = response[1 + response[0]] - if result['confirmationType'] == 0x02: - result['keycardData'] = response[1 + response[0] + 1:] - if result['confirmationType'] == 0x03: - offset = 1 + response[0] + 1 - keycardDataLength = response[offset] - offset = offset + 1 - result['keycardData'] = response[offset : offset + keycardDataLength] - offset = offset + keycardDataLength - result['secureScreenData'] = response[offset:] - if result['confirmationType'] == 0x04: - offset = 1 + response[0] + 1 - keycardDataLength = response[offset] - result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] - if outputs == None: - result['outputData'] = response[1 : 1 + response[0]] - else: - result['outputData'] = outputs - return result - - def finalizeInputFull(self, outputData): - result = {} - offset = 0 - encryptedOutputData = b"" - while (offset < len(outputData)): - blockLength = self.scriptBlockLength - if ((offset + blockLength) < len(outputData)): - dataLength = blockLength - p1 = 0x00 - else: - dataLength = len(outputData) - offset - p1 = 0x80 - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ - p1, 0x00, dataLength ] - apdu.extend(outputData[offset : offset + dataLength]) - _, response = cmd.transport.exchange_raw(bytearray(apdu)) - encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] - offset += dataLength - if len(response) > 1: - result['confirmationNeeded'] = response[1 + response[0]] != 0x00 - result['confirmationType'] = response[1 + response[0]] - else: - # Support for old style API before 1.0.2 - result['confirmationNeeded'] = response[0] != 0x00 - result['confirmationType'] = response[0] - if result['confirmationType'] == 0x02: - result['keycardData'] = response[1 + response[0] + 1:] # legacy - if result['confirmationType'] == 0x03: - offset = 1 + response[0] + 1 - keycardDataLength = response[offset] - offset = offset + 1 - result['keycardData'] = response[offset : offset + keycardDataLength] - offset = offset + keycardDataLength - result['secureScreenData'] = response[offset:] - result['encryptedOutputData'] = encryptedOutputData - if result['confirmationType'] == 0x04: - offset = 1 + response[0] + 1 - keycardDataLength = response[offset] - result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] - return result - - @classmethod - def untrustedHashSign(self, cmd, path, pin="", lockTime=0, sighashType=0x01): - if isinstance(pin, str): - pin = pin.encode('utf-8') - donglePath = parse_bip32_path(path) - - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_SIGN, 0x00, 0x00 ] - params = [] - params.extend(donglePath) - params.append(len(pin)) - params.extend(bytearray(pin)) - writeUint32BE(lockTime, params) - params.append(sighashType) - apdu.append(len(params)) - apdu.extend(params) - _,result = cmd.transport.exchange_raw(bytearray(apdu)) - result = bytearray(result) - result[0] = 0x30 - return result - - def signMessagePrepareV1(self, path, message): - donglePath = parse_bip32_path(path) - if self.needKeyCache: - self.resolvePublicKeysInPath(path) - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, 0x00 ] - params = [] - params.extend(donglePath) - params.append(len(message)) - params.extend(bytearray(message)) - apdu.append(len(params)) - apdu.extend(params) - response = self.dongle.exchange(bytearray(apdu)) - result['confirmationNeeded'] = response[0] != 0x00 - result['confirmationType'] = response[0] - if result['confirmationType'] == 0x02: - result['keycardData'] = response[1:] - if result['confirmationType'] == 0x03: - result['secureScreenData'] = response[1:] - return result - - def signMessagePrepareV2(self, path, message): - donglePath = parse_bip32_path(path) - if self.needKeyCache: - self.resolvePublicKeysInPath(path) - result = {} - offset = 0 - encryptedOutputData = b"" - while (offset < len(message)): - params = []; - if offset == 0: - params.extend(donglePath) - params.append((len(message) >> 8) & 0xff) - params.append(len(message) & 0xff) - p2 = 0x01 - else: - p2 = 0x80 - blockLength = 255 - len(params) - if ((offset + blockLength) < len(message)): - dataLength = blockLength - else: - dataLength = len(message) - offset - params.extend(bytearray(message[offset : offset + dataLength])) - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, p2 ] - apdu.append(len(params)) - apdu.extend(params) - response = self.dongle.exchange(bytearray(apdu)) - encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] - offset += blockLength - result['confirmationNeeded'] = response[1 + response[0]] != 0x00 - result['confirmationType'] = response[1 + response[0]] - if result['confirmationType'] == 0x03: - offset = 1 + response[0] + 1 - result['secureScreenData'] = response[offset:] - result['encryptedOutputData'] = encryptedOutputData - - return result - - def signMessagePrepare(self, path, message): - try: - result = self.signMessagePrepareV2(path, message) - except BTChipException as e: - if (e.sw == 0x6b00): # Old firmware version, try older method - result = self.signMessagePrepareV1(path, message) - else: - raise - return result - - def signMessageSign(self, pin=""): - if isinstance(pin, str): - pin = pin.encode('utf-8') - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x80, 0x00 ] - params = [] - if pin is not None: - params.append(len(pin)) - params.extend(bytearray(pin)) - else: - params.append(0x00) - apdu.append(len(params)) - apdu.extend(params) - response = self.dongle.exchange(bytearray(apdu)) - return response - - def setup(self, operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH, userPin, wipePin, keymapEncoding, seed=None, developerKey=None): - if isinstance(userPin, str): - userPin = userPin.encode('utf-8') - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SETUP, 0x00, 0x00 ] - params = [ operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH ] - params.append(len(userPin)) - params.extend(bytearray(userPin)) - if wipePin is not None: - if isinstance(wipePin, str): - wipePin = wipePin.encode('utf-8') - params.append(len(wipePin)) - params.extend(bytearray(wipePin)) - else: - params.append(0x00) - if seed is not None: - if len(seed) < 32 or len(seed) > 64: - raise BTChipException("Invalid seed length") - params.append(len(seed)) - params.extend(seed) - else: - params.append(0x00) - if developerKey is not None: - params.append(len(developerKey)) - params.extend(developerKey) - else: - params.append(0x00) - apdu.append(len(params)) - apdu.extend(params) - response = self.dongle.exchange(bytearray(apdu)) - result['trustedInputKey'] = response[0:16] - result['developerKey'] = response[16:] - self.setKeymapEncoding(keymapEncoding) - try: - self.setTypingBehaviour(0xff, 0xff, 0xff, 0x10) - except BTChipException as e: - if (e.sw == 0x6700): # Old firmware version, command not supported - pass - else: - raise - return result - - def setKeymapEncoding(self, keymapEncoding): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x00, 0x00 ] - apdu.append(len(keymapEncoding)) - apdu.extend(keymapEncoding) - self.dongle.exchange(bytearray(apdu)) - - def setTypingBehaviour(self, unitDelayStart, delayStart, unitDelayKey, delayKey): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x01, 0x00 ] - params = [] - writeUint32BE(unitDelayStart, params) - writeUint32BE(delayStart, params) - writeUint32BE(unitDelayKey, params) - writeUint32BE(delayKey, params) - apdu.append(len(params)) - apdu.extend(params) - self.dongle.exchange(bytearray(apdu)) - - def getOperationMode(self): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_OPERATION_MODE, 0x00, 0x00, 0x00] - response = self.dongle.exchange(bytearray(apdu)) - return response[0] - - def setOperationMode(self, operationMode): - if operationMode != btchip.OPERATION_MODE_WALLET \ - and operationMode != btchip.OPERATION_MODE_RELAXED_WALLET \ - and operationMode != btchip.OPERATION_MODE_SERVER \ - and operationMode != btchip.OPERATION_MODE_DEVELOPER: - raise BTChipException("Invalid operation mode") - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, 0x00, 0x00, 0x01, operationMode ] - self.dongle.exchange(bytearray(apdu)) - - @classmethod - def enableAlternate2fa(self, cmd, persistent): - if persistent: - p1 = 0x02 - else: - p1 = 0x01 - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, p1, 0x00, 0x01, btchip.OPERATION_MODE_WALLET ] - cmd.transport.exchange_raw(bytearray(apdu)) - - def getFirmwareVersion(self): - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_FIRMWARE_VERSION, 0x00, 0x00, 0x00 ] - try: - response = self.dongle.exchange(bytearray(apdu)) - except BTChipException as e: - if (e.sw == 0x6985): - response = [0x00, 0x00, 0x01, 0x04, 0x03 ] - pass - else: - raise - result['compressedKeys'] = (response[0] == 0x01) - result['version'] = "%d.%d.%d" % (response[2], response[3], response[4]) - result['specialVersion'] = response[1] - return result - - def getRandom(self, size): - if size > 255: - raise BTChipException("Invalid size") - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_RANDOM, 0x00, 0x00, size ] - return self.dongle.exchange(bytearray(apdu)) - - def getPOSSeedKey(self): - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x01, 0x00, 0x00 ] - return self.dongle.exchange(bytearray(apdu)) - - def getPOSEncryptedSeed(self): - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x02, 0x00, 0x00 ] - return self.dongle.exchange(bytearray(apdu)) - - def importPrivateKey(self, data, isSeed=False): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_IMPORT_PRIVATE_KEY, (0x02 if isSeed else 0x01), 0x00 ] - apdu.append(len(data)) - apdu.extend(data) - return self.dongle.exchange(bytearray(apdu)) - - def getPublicKey(self, encodedPrivateKey): - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(encodedPrivateKey) + 1) - apdu.append(len(encodedPrivateKey)) - apdu.extend(encodedPrivateKey) - response = self.dongle.exchange(bytearray(apdu)) - offset = 1 - result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] - offset = offset + 1 + response[offset] - if response[0] == 0x02: - result['chainCode'] = response[offset : offset + 32] - offset = offset + 32 - result['depth'] = response[offset] - offset = offset + 1 - result['parentFingerprint'] = response[offset : offset + 4] - offset = offset + 4 - result['childNumber'] = response[offset : offset + 4] - return result - - def deriveBip32Key(self, encodedPrivateKey, path): - donglePath = parse_bip32_path(path) - if self.needKeyCache: - self.resolvePublicKeysInPath(path) - offset = 1 - currentEncodedPrivateKey = encodedPrivateKey - while (offset < len(donglePath)): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_DERIVE_BIP32_KEY, 0x00, 0x00 ] - apdu.append(len(currentEncodedPrivateKey) + 1 + 4) - apdu.append(len(currentEncodedPrivateKey)) - apdu.extend(currentEncodedPrivateKey) - apdu.extend(donglePath[offset : offset + 4]) - currentEncodedPrivateKey = self.dongle.exchange(bytearray(apdu)) - offset = offset + 4 - return currentEncodedPrivateKey - - def signImmediate(self, encodedPrivateKey, data, deterministic=True): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGNVERIFY_IMMEDIATE, 0x00, (0x80 if deterministic else 0x00) ] - apdu.append(len(encodedPrivateKey) + len(data) + 2); - apdu.append(len(encodedPrivateKey)) - apdu.extend(encodedPrivateKey) - apdu.append(len(data)) - apdu.extend(data) - return self.dongle.exchange(bytearray(apdu)) - -# Functions dedicated to the Java Card interface when no proprietary API is available - - def parse_bip32_path_internal(self, path): - if len(path) == 0: - return [] - result = [] - elements = path.split('/') - for pathElement in elements: - element = pathElement.split('\'') - if len(element) == 1: - result.append(int(element[0])) - else: - result.append(0x80000000 | int(element[0])) - return result - - def serialize_bip32_path_internal(self, path): - result = [] - for pathElement in path: - writeUint32BE(pathElement, result) - return bytearray([ len(path) ] + result) - - def resolvePublicKey(self, path): - expandedPath = self.serialize_bip32_path_internal(path) - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(expandedPath)) - apdu.extend(expandedPath) - result = self.dongle.exchange(bytearray(apdu)) - if (result[0] == 0): - # Not present, need to be inserted into the cache - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(expandedPath)) - apdu.extend(expandedPath) - result = self.dongle.exchange(bytearray(apdu)) - hashData = result[0:32] - keyX = result[32:64] - signature = result[64:] - keyXY = recoverKey(signature, hashData, keyX) - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(expandedPath) + 65) - apdu.extend(expandedPath) - apdu.extend(keyXY) - self.dongle.exchange(bytearray(apdu)) - - def resolvePublicKeysInPath(self, path): - splitPath = self.parse_bip32_path_internal(path) - # Locate the first public key in path - offset = 0 - startOffset = 0 - while(offset < len(splitPath)): - if (splitPath[offset] < 0x80000000): - startOffset = offset - break - offset = offset + 1 - if startOffset != 0: - searchPath = splitPath[0:startOffset - 1] - offset = startOffset - 1 - while(offset < len(splitPath)): - searchPath = searchPath + [ splitPath[offset] ] - self.resolvePublicKey(searchPath) - offset = offset + 1 - self.resolvePublicKey(splitPath) - - def getJCExtendedFeatures(self): - result = {} - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_GET_FEATURES, 0x00, 0x00, 0x00 ] - response = self.dongle.exchange(bytearray(apdu)) - result['proprietaryApi'] = ((response[0] & 0x01) != 0) - return result diff --git a/tests/btchippython/btchip/btchipComm.py b/tests/btchippython/btchip/btchipComm.py deleted file mode 100644 index 3a5fe001..00000000 --- a/tests/btchippython/btchip/btchipComm.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from abc import ABCMeta, abstractmethod -from .btchipException import * -from .ledgerWrapper import wrapCommandAPDU, unwrapResponseAPDU -from binascii import hexlify -import time -import os -import struct -import socket - -try: - import hid - HID = True -except ImportError: - HID = False - -try: - from smartcard.Exceptions import NoCardException - from smartcard.System import readers - from smartcard.util import toHexString, toBytes - SCARD = True -except ImportError: - SCARD = False - -class DongleWait(object): - __metaclass__ = ABCMeta - - @abstractmethod - def waitFirstResponse(self, timeout): - pass - -class Dongle(object): - __metaclass__ = ABCMeta - - @abstractmethod - def exchange(self, apdu, timeout=20000): - pass - - @abstractmethod - def close(self): - pass - - def setWaitImpl(self, waitImpl): - self.waitImpl = waitImpl - -class HIDDongleHIDAPI(Dongle, DongleWait): - - def __init__(self, device, ledger=False, debug=False): - self.device = device - self.ledger = ledger - self.debug = debug - self.waitImpl = self - self.opened = True - - def exchange(self, apdu, timeout=20000): - if self.debug: - print("=> %s" % hexlify(apdu)) - if self.ledger: - apdu = wrapCommandAPDU(0x0101, apdu, 64) - padSize = len(apdu) % 64 - tmp = apdu - if padSize != 0: - tmp.extend([0] * (64 - padSize)) - offset = 0 - while(offset != len(tmp)): - data = tmp[offset:offset + 64] - data = bytearray([0]) + data - self.device.write(data) - offset += 64 - dataLength = 0 - dataStart = 2 - result = self.waitImpl.waitFirstResponse(timeout) - if not self.ledger: - if result[0] == 0x61: # 61xx : data available - self.device.set_nonblocking(False) - dataLength = result[1] - dataLength += 2 - if dataLength > 62: - remaining = dataLength - 62 - while(remaining != 0): - if remaining > 64: - blockLength = 64 - else: - blockLength = remaining - result.extend(bytearray(self.device.read(65))[0:blockLength]) - remaining -= blockLength - swOffset = dataLength - dataLength -= 2 - self.device.set_nonblocking(True) - else: - swOffset = 0 - else: - self.device.set_nonblocking(False) - while True: - response = unwrapResponseAPDU(0x0101, result, 64) - if response is not None: - result = response - dataStart = 0 - swOffset = len(response) - 2 - dataLength = len(response) - 2 - self.device.set_nonblocking(True) - break - result.extend(bytearray(self.device.read(65))) - sw = (result[swOffset] << 8) + result[swOffset + 1] - response = result[dataStart : dataLength + dataStart] - if self.debug: - print("<= %s%.2x" % (hexlify(response), sw)) - if sw != 0x9000: - raise BTChipException("Invalid status %04x" % sw, sw) - return response - - def waitFirstResponse(self, timeout): - start = time.time() - data = "" - while len(data) == 0: - data = self.device.read(65) - if not len(data): - if time.time() - start > timeout: - raise BTChipException("Timeout") - time.sleep(0.02) - return bytearray(data) - - def close(self): - if self.opened: - try: - self.device.close() - except Exception: - pass - self.opened = False - -class DongleSmartcard(Dongle): - - def __init__(self, device, debug=False): - self.device = device - self.debug = debug - self.waitImpl = self - self.opened = True - - def exchange(self, apdu, timeout=20000): - if self.debug: - print("=> %s" % hexlify(apdu)) - response, sw1, sw2 = self.device.transmit(toBytes(hexlify(apdu))) - sw = (sw1 << 8) | sw2 - if self.debug: - print("<= %s%.2x" % (toHexString(response).replace(" ", ""), sw)) - if sw != 0x9000: - raise BTChipException("Invalid status %04x" % sw, sw) - return bytearray(response) - - def close(self): - if self.opened: - try: - self.device.disconnect() - except Exception: - pass - self.opened = False - -class DongleServer(Dongle): - - def __init__(self, server, port, debug=False): - self.server = server - self.port = port - self.debug = debug - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - self.socket.connect((self.server, self.port)) - except Exception: - raise BTChipException("Proxy connection failed") - - def exchange(self, apdu, timeout=20000): - if self.debug: - print("=> %s" % hexlify(apdu)) - self.socket.send(struct.pack(">I", len(apdu))) - self.socket.send(apdu) - size = struct.unpack(">I", self.socket.recv(4))[0] - response = self.socket.recv(size) - sw = struct.unpack(">H", self.socket.recv(2))[0] - if self.debug: - print("<= %s%.2x" % (hexlify(response), sw)) - if sw != 0x9000: - raise BTChipException("Invalid status %04x" % sw, sw) - return bytearray(response) - - def close(self): - try: - self.socket.close() - except Exception: - pass - -def getDongle(debug=False): - dev = None - hidDevicePath = None - ledger = False - if HID: - for hidDevice in hid.enumerate(0, 0): - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x2b7c: - hidDevicePath = hidDevice['path'] - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x3b7c: - hidDevicePath = hidDevice['path'] - ledger = True - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x4b7c: - hidDevicePath = hidDevice['path'] - ledger = True - if hidDevice['vendor_id'] == 0x2c97: - if ('interface_number' in hidDevice and hidDevice['interface_number'] == 0) or ('usage_page' in hidDevice and hidDevice['usage_page'] == 0xffa0): - hidDevicePath = hidDevice['path'] - ledger = True - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x1807: - hidDevicePath = hidDevice['path'] - if hidDevicePath is not None: - dev = hid.device() - dev.open_path(hidDevicePath) - dev.set_nonblocking(True) - return HIDDongleHIDAPI(dev, ledger, debug) - - if SCARD: - connection = None - for reader in readers(): - try: - connection = reader.createConnection() - connection.connect() - response, sw1, sw2 = connection.transmit(toBytes("00A4040010FF4C4547522E57414C5430312E493031")) - sw = (sw1 << 8) | sw2 - if sw == 0x9000: - break - else: - connection.disconnect() - connection = None - except Exception: - connection = None - pass - if connection is not None: - return DongleSmartcard(connection, debug) - if (os.getenv("LEDGER_PROXY_ADDRESS") is not None) and (os.getenv("LEDGER_PROXY_PORT") is not None): - return DongleServer(os.getenv("LEDGER_PROXY_ADDRESS"), int(os.getenv("LEDGER_PROXY_PORT")), debug) - raise BTChipException("No dongle found") diff --git a/tests/btchippython/btchip/btchipException.py b/tests/btchippython/btchip/btchipException.py deleted file mode 100644 index ec577289..00000000 --- a/tests/btchippython/btchip/btchipException.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -class BTChipException(Exception): - - def __init__(self, message, sw=0x6f00): - self.message = message - self.sw = sw - - def __str__(self): - buf = "Exception : " + self.message - return buf diff --git a/tests/btchippython/btchip/btchipFirmwareWizard.py b/tests/btchippython/btchip/btchipFirmwareWizard.py deleted file mode 100644 index 06522110..00000000 --- a/tests/btchippython/btchip/btchipFirmwareWizard.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -def checkFirmware(version): - return True - -def updateFirmware(): - raise Exception("Unsupported BTChip firmware - please update your firmware from https://firmwareupdate.hardwarewallet.com") diff --git a/tests/btchippython/btchip/btchipHelpers.py b/tests/btchippython/btchip/btchipHelpers.py deleted file mode 100644 index ba5b66c5..00000000 --- a/tests/btchippython/btchip/btchipHelpers.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -import decimal -import re - -# from pycoin -SATOSHI_PER_COIN = decimal.Decimal(1e8) -COIN_PER_SATOSHI = decimal.Decimal(1)/SATOSHI_PER_COIN - -def satoshi_to_btc(satoshi_count): - if satoshi_count == 0: - return decimal.Decimal(0) - r = satoshi_count * COIN_PER_SATOSHI - return r.normalize() - -def btc_to_satoshi(btc): - return int(decimal.Decimal(btc) * SATOSHI_PER_COIN) -# /from pycoin - -def writeUint32BE(value, buffer): - buffer.append((value >> 24) & 0xff) - buffer.append((value >> 16) & 0xff) - buffer.append((value >> 8) & 0xff) - buffer.append(value & 0xff) - return buffer - -def writeUint32LE(value, buffer): - buffer.append(value & 0xff) - buffer.append((value >> 8) & 0xff) - buffer.append((value >> 16) & 0xff) - buffer.append((value >> 24) & 0xff) - return buffer - -def writeHexAmount(value, buffer): - buffer.append(value & 0xff) - buffer.append((value >> 8) & 0xff) - buffer.append((value >> 16) & 0xff) - buffer.append((value >> 24) & 0xff) - buffer.append((value >> 32) & 0xff) - buffer.append((value >> 40) & 0xff) - buffer.append((value >> 48) & 0xff) - buffer.append((value >> 56) & 0xff) - return buffer - -def writeHexAmountBE(value, buffer): - buffer.append((value >> 56) & 0xff) - buffer.append((value >> 48) & 0xff) - buffer.append((value >> 40) & 0xff) - buffer.append((value >> 32) & 0xff) - buffer.append((value >> 24) & 0xff) - buffer.append((value >> 16) & 0xff) - buffer.append((value >> 8) & 0xff) - buffer.append(value & 0xff) - return buffer - -def parse_bip32_path(path): - if len(path) == 0: - return bytearray([ 0 ]) - result = [] - elements = path.split('/') - if len(elements) > 10: - raise BTChipException("Path too long") - for pathElement in elements: - element = re.split('\'|h|H', pathElement) - if len(element) == 1: - writeUint32BE(int(element[0]), result) - else: - writeUint32BE(0x80000000 | int(element[0]), result) - return bytearray([ len(elements) ] + result) diff --git a/tests/btchippython/btchip/btchipKeyRecovery.py b/tests/btchippython/btchip/btchipKeyRecovery.py deleted file mode 100644 index 20fa7705..00000000 --- a/tests/btchippython/btchip/btchipKeyRecovery.py +++ /dev/null @@ -1,57 +0,0 @@ -# From Electrum - -import ecdsa -from ecdsa.curves import SECP256k1 -from ecdsa.ellipticcurve import Point -from ecdsa.util import string_to_number, number_to_string - -class MyVerifyingKey(ecdsa.VerifyingKey): - @classmethod - def from_signature(klass, sig, recid, h, curve): - """ See http://www.secg.org/download/aid-780/sec1-v2.pdf, chapter 4.1.6 """ - from ecdsa import util, numbertheory - import msqr - curveFp = curve.curve - G = curve.generator - order = G.order() - # extract r,s from signature - r, s = util.sigdecode_string(sig, order) - # 1.1 - x = r + (recid/2) * order - # 1.3 - alpha = ( x * x * x + curveFp.a() * x + curveFp.b() ) % curveFp.p() - beta = msqr.modular_sqrt(alpha, curveFp.p()) - y = beta if (beta - recid) % 2 == 0 else curveFp.p() - beta - # 1.4 the constructor checks that nR is at infinity - R = Point(curveFp, x, y, order) - # 1.5 compute e from message: - e = string_to_number(h) - minus_e = -e % order - # 1.6 compute Q = r^-1 (sR - eG) - inv_r = numbertheory.inverse_mod(r,order) - Q = inv_r * ( s * R + minus_e * G ) - return klass.from_public_point( Q, curve ) - -def point_to_ser(P): - return ( '04'+('%064x'%P.x())+('%064x'%P.y()) ).decode('hex') - -def recoverKey(signature, hashValue, keyX): - rLength = signature[3] - r = signature[4 : 4 + rLength] - sLength = signature[4 + rLength + 1] - s = signature[4 + rLength + 2:] - if rLength == 33: - r = r[1:] - if sLength == 33: - s = s[1:] - r = str(r) - s = str(s) - for i in range(4): - try: - key = MyVerifyingKey.from_signature(r + s, i, hashValue, curve = SECP256k1) - candidate = point_to_ser(key.pubkey.point) - if candidate[1:33] == keyX: - return candidate - except Exception: - pass - raise Exception("Key recovery failed") diff --git a/tests/btchippython/btchip/btchipPersoWizard.py b/tests/btchippython/btchip/btchipPersoWizard.py deleted file mode 100644 index a157410d..00000000 --- a/tests/btchippython/btchip/btchipPersoWizard.py +++ /dev/null @@ -1,376 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -import sys - -from PyQt4 import QtCore, QtGui -from PyQt4.QtGui import QDialog, QMessageBox - -try: - from mnemonic import Mnemonic - MNEMONIC = True -except Exception: - MNEMONIC = False - -from .btchipComm import getDongle, DongleWait -from .btchip import btchip -from .btchipUtils import compress_public_key,format_transaction, get_regular_input_script -from .bitcoinTransaction import bitcoinTransaction -from .btchipException import BTChipException - -import ui.personalization00start -import ui.personalization01seed -import ui.personalization02security -import ui.personalization03config -import ui.personalization04finalize -import ui.personalizationseedbackup01 -import ui.personalizationseedbackup02 -import ui.personalizationseedbackup03 -import ui.personalizationseedbackup04 - -BTCHIP_DEBUG = False - -def waitDongle(currentDialog, persoData): - try: - if persoData['client'] != None: - try: - persoData['client'].dongle.close() - except Exception: - pass - dongle = getDongle(BTCHIP_DEBUG) - persoData['client'] = btchip(dongle) - persoData['client'].getFirmwareVersion()['version'].split(".") - return True - except BTChipException as e: - if e.sw == 0x6faa: - QMessageBox.information(currentDialog, "BTChip Setup", "Please unplug the dongle and plug it again", "OK") - return False - if QMessageBox.question(currentDialog, "BTChip setup", "BTChip dongle not found. It might be in the wrong mode. Try unplugging und plugging it back in again, then press 'OK'", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes: - return False - else: - raise Exception("Aborted by user") - except Exception as e: - if QMessageBox.question(currentDialog, "BTChip setup", "BTChip dongle not found. It might be in the wrong mode. Try unplugging und plugging it back in again, then press 'OK'", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes: - return False - else: - raise Exception("Aborted by user") - - -class StartBTChipPersoDialog(QtGui.QDialog): - - def __init__(self): - QDialog.__init__(self, None) - self.ui = ui.personalization00start.Ui_Dialog() - self.ui.setupUi(self) - self.ui.NextButton.clicked.connect(self.processNext) - self.ui.CancelButton.clicked.connect(self.processCancel) - - def processNext(self): - persoData = {} - persoData['currencyCode'] = 0x00 - persoData['currencyCodeP2SH'] = 0x05 - persoData['client'] = None - dialog = SeedDialog(persoData, self) - persoData['main'] = self - dialog.exec_() - pass - - def processCancel(self): - self.reject() - -class SeedDialog(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalization01seed.Ui_Dialog() - self.ui.setupUi(self) - self.ui.seed.setEnabled(False) - self.ui.RestoreWalletButton.toggled.connect(self.restoreWalletToggled) - self.ui.NextButton.clicked.connect(self.processNext) - self.ui.CancelButton.clicked.connect(self.processCancel) - if MNEMONIC: - self.mnemonic = Mnemonic('english') - self.ui.mnemonicNotAvailableLabel.hide() - - def restoreWalletToggled(self, toggled): - self.ui.seed.setEnabled(toggled) - - def processNext(self): - self.persoData['seed'] = None - if self.ui.RestoreWalletButton.isChecked(): - # Check if it's an hexa string - seedText = str(self.ui.seed.text()) - if len(seedText) == 0: - QMessageBox.warning(self, "Error", "Please enter a seed", "OK") - return - if seedText[-1] == 'X': - seedText = seedText[0:-1] - try: - self.persoData['seed'] = seedText.decode('hex') - except Exception: - pass - if self.persoData['seed'] == None: - if not MNEMONIC: - QMessageBox.warning(self, "Error", "Mnemonic API not available. Please install https://github.com/trezor/python-mnemonic", "OK") - return - if not self.mnemonic.check(seedText): - QMessageBox.warning(self, "Error", "Invalid mnemonic", "OK") - return - self.persoData['seed'] = Mnemonic.to_seed(seedText) - else: - if (len(self.persoData['seed']) < 32) or (len(self.persoData['seed']) > 64): - QMessageBox.warning(self, "Error", "Invalid seed length", "OK") - return - dialog = SecurityDialog(self.persoData, self) - self.hide() - dialog.exec_() - - def processCancel(self): - self.reject() - self.persoData['main'].reject() - -class SecurityDialog(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalization02security.Ui_Dialog() - self.ui.setupUi(self) - self.ui.NextButton.clicked.connect(self.processNext) - self.ui.CancelButton.clicked.connect(self.processCancel) - - - def processNext(self): - if (self.ui.pin1.text() != self.ui.pin2.text()): - self.ui.pin1.setText("") - self.ui.pin2.setText("") - QMessageBox.warning(self, "Error", "PINs are not matching", "OK") - return - if (len(self.ui.pin1.text()) < 4): - QMessageBox.warning(self, "Error", "PIN must be at least 4 characteres long", "OK") - return - if (len(self.ui.pin1.text()) > 32): - QMessageBox.warning(self, "Error", "PIN is too long", "OK") - return - self.persoData['pin'] = str(self.ui.pin1.text()) - self.persoData['hardened'] = self.ui.HardenedButton.isChecked() - dialog = ConfigDialog(self.persoData, self) - self.hide() - dialog.exec_() - - def processCancel(self): - self.reject() - self.persoData['main'].reject() - -class ConfigDialog(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalization03config.Ui_Dialog() - self.ui.setupUi(self) - self.ui.NextButton.clicked.connect(self.processNext) - self.ui.CancelButton.clicked.connect(self.processCancel) - - def processNext(self): - if (self.ui.qwertyButton.isChecked()): - self.persoData['keyboard'] = btchip.QWERTY_KEYMAP - elif (self.ui.qwertzButton.isChecked()): - self.persoData['keyboard'] = btchip.QWERTZ_KEYMAP - elif (self.ui.azertyButton.isChecked()): - self.persoData['keyboard'] = btchip.AZERTY_KEYMAP - try: - while not waitDongle(self, self.persoData): - pass - except Exception as e: - self.reject() - self.persoData['main'].reject() - mode = btchip.OPERATION_MODE_WALLET - if not self.persoData['hardened']: - mode = mode | btchip.OPERATION_MODE_SERVER - try: - self.persoData['client'].setup(mode, btchip.FEATURE_RFC6979, self.persoData['currencyCode'], - self.persoData['currencyCodeP2SH'], self.persoData['pin'], None, - self.persoData['keyboard'], self.persoData['seed']) - except BTChipException as e: - if e.sw == 0x6985: - QMessageBox.warning(self, "Error", "Dongle is already set up. Please insert a different one", "OK") - return - except Exception as e: - QMessageBox.warning(self, "Error", "Error performing setup", "OK") - return - if self.persoData['seed'] is None: - dialog = SeedBackupStart(self.persoData, self) - self.hide() - dialog.exec_() - else: - dialog = FinalizeDialog(self.persoData, self) - self.hide() - dialog.exec_() - - def processCancel(self): - self.reject() - self.persoData['main'].reject() - -class FinalizeDialog(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalization04finalize.Ui_Dialog() - self.ui.setupUi(self) - self.ui.FinishButton.clicked.connect(self.finish) - try: - while not waitDongle(self, self.persoData): - pass - except Exception as e: - self.reject() - self.persoData['main'].reject() - attempts = self.persoData['client'].getVerifyPinRemainingAttempts() - self.ui.remainingAttemptsLabel.setText("Remaining attempts " + str(attempts)) - - def finish(self): - if (len(self.ui.pin1.text()) < 4): - QMessageBox.warning(self, "Error", "PIN must be at least 4 characteres long", "OK") - return - if (len(self.ui.pin1.text()) > 32): - QMessageBox.warning(self, "Error", "PIN is too long", "OK") - return - try: - self.persoData['client'].verifyPin(str(self.ui.pin1.text())) - except BTChipException as e: - if ((e.sw == 0x63c0) or (e.sw == 0x6985)): - QMessageBox.warning(self, "Error", "Invalid PIN - dongle has been reset. Please personalize again", "OK") - self.reject() - self.persoData['main'].reject() - if ((e.sw & 0xfff0) == 0x63c0): - attempts = e.sw - 0x63c0 - self.ui.remainingAttemptsLabel.setText("Remaining attempts " + str(attempts)) - QMessageBox.warning(self, "Error", "Invalid PIN - please unplug the dongle and plug it again before retrying", "OK") - try: - while not waitDongle(self, self.persoData): - pass - except Exception as e: - self.reject() - self.persoData['main'].reject() - return - except Exception as e: - QMessageBox.warning(self, "Error", "Unexpected error verifying PIN - aborting", "OK") - self.reject() - self.persoData['main'].reject() - return - if not self.persoData['hardened']: - try: - self.persoData['client'].setOperationMode(btchip.OPERATION_MODE_SERVER) - except Exception: - QMessageBox.warning(self, "Error", "Error switching to non hardened mode", "OK") - self.reject() - self.persoData['main'].reject() - return - QMessageBox.information(self, "BTChip Setup", "Setup completed. Please unplug the dongle and plug it again before use", "OK") - self.accept() - self.persoData['main'].accept() - -class SeedBackupStart(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalizationseedbackup01.Ui_Dialog() - self.ui.setupUi(self) - self.ui.NextButton.clicked.connect(self.processNext) - - def processNext(self): - dialog = SeedBackupUnplug(self.persoData, self) - self.hide() - dialog.exec_() - -class SeedBackupUnplug(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalizationseedbackup02.Ui_Dialog() - self.ui.setupUi(self) - self.ui.NextButton.clicked.connect(self.processNext) - - def processNext(self): - dialog = SeedBackupInstructions(self.persoData, self) - self.hide() - dialog.exec_() - -class SeedBackupInstructions(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalizationseedbackup03.Ui_Dialog() - self.ui.setupUi(self) - self.ui.NextButton.clicked.connect(self.processNext) - - def processNext(self): - dialog = SeedBackupVerify(self.persoData, self) - self.hide() - dialog.exec_() - -class SeedBackupVerify(QtGui.QDialog): - - def __init__(self, persoData, parent = None): - QDialog.__init__(self, parent) - self.persoData = persoData - self.ui = ui.personalizationseedbackup04.Ui_Dialog() - self.ui.setupUi(self) - self.ui.seedOkButton.clicked.connect(self.seedOK) - self.ui.seedKoButton.clicked.connect(self.seedKO) - - def seedOK(self): - dialog = FinalizeDialog(self.persoData, self) - self.hide() - dialog.exec_() - - def seedKO(self): - finished = False - while not finished: - try: - while not waitDongle(self, self.persoData): - pass - except Exception as e: - pass - try: - self.persoData['client'].verifyPin("0") - except BTChipException as e: - if e.sw == 0x63c0: - QMessageBox.information(self, "BTChip Setup", "Dongle is reset and can be repersonalized", "OK") - finished = True - pass - if e.sw == 0x6faa: - QMessageBox.information(self, "BTChip Setup", "Please unplug the dongle and plug it again", "OK") - pass - except Exception as e: - pass - self.reject() - self.persoData['main'].reject() - -if __name__ == "__main__": - - app = QtGui.QApplication(sys.argv) - dialog = StartBTChipPersoDialog() - dialog.show() - app.exec_() diff --git a/tests/btchippython/btchip/btchipUtils.py b/tests/btchippython/btchip/btchipUtils.py deleted file mode 100644 index 0bf2fa2a..00000000 --- a/tests/btchippython/btchip/btchipUtils.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from .btchipException import * -from .bitcoinTransaction import * -from .btchipHelpers import * - -def compress_public_key(publicKey): - if publicKey[0] == 0x04: - if (publicKey[64] & 1) != 0: - prefix = 0x03 - else: - prefix = 0x02 - result = [prefix] - result.extend(publicKey[1:33]) - return bytearray(result) - elif publicKey[0] == 0x03 or publicKey[0] == 0x02: - return publicKey - else: - raise BTChipException("Invalid public key format") - -def format_transaction(dongleOutputData, trustedInputsAndInputScripts, version=0x01, lockTime=0): - transaction = bitcoinTransaction() - transaction.version = [] - writeUint32LE(version, transaction.version) - for item in trustedInputsAndInputScripts: - newInput = bitcoinInput() - newInput.prevOut = item[0][4:4+36] - newInput.script = item[1] - if len(item) > 2: - newInput.sequence = bytearray(item[2].decode('hex')) - else: - newInput.sequence = bytearray([0xff, 0xff, 0xff, 0xff]) - transaction.inputs.append(newInput) - result = transaction.serialize(True) - result.extend(dongleOutputData) - writeUint32LE(lockTime, result) - return bytearray(result) - -def get_regular_input_script(sigHashtype, publicKey): - if len(sigHashtype) >= 0x4c: - raise BTChipException("Invalid sigHashtype") - if len(publicKey) >= 0x4c: - raise BTChipException("Invalid publicKey") - result = [ len(sigHashtype) ] - result.extend(sigHashtype) - result.append(len(publicKey)) - result.extend(publicKey) - return bytearray(result) - -def write_pushed_data_size(data, buffer): - if (len(data) > 0xffff): - raise BTChipException("unsupported encoding") - if (len(data) < 0x4c): - buffer.append(len(data)) - elif (len(data) > 255): - buffer.append(0x4d) - buffer.append(len(data) & 0xff) - buffer.append((len(data) >> 8) & 0xff) - else: - buffer.append(0x4c) - buffer.append(len(data)) - return buffer - - -def get_p2sh_input_script(redeemScript, sigHashtypeList): - result = [ 0x00 ] - for sigHashtype in sigHashtypeList: - write_pushed_data_size(sigHashtype, result) - result.extend(sigHashtype) - write_pushed_data_size(redeemScript, result) - result.extend(redeemScript) - return bytearray(result) - -def get_p2pk_input_script(sigHashtype): - if len(sigHashtype) >= 0x4c: - raise BTChipException("Invalid sigHashtype") - result = [ len(sigHashtype) ] - result.extend(sigHashtype) - return bytearray(result) - -def get_output_script(amountScriptArray): - result = [ len(amountScriptArray) ] - for amountScript in amountScriptArray: - writeHexAmount(btc_to_satoshi(str(amountScript[0])), result) - writeVarint(len(amountScript[1]), result) - result.extend(amountScript[1]) - return bytearray(result) - diff --git a/tests/btchippython/btchip/ledgerWrapper.py b/tests/btchippython/btchip/ledgerWrapper.py deleted file mode 100644 index 44fde1db..00000000 --- a/tests/btchippython/btchip/ledgerWrapper.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -import struct -from .btchipException import BTChipException - -def wrapCommandAPDU(channel, command, packetSize): - if packetSize < 3: - raise BTChipException("Can't handle Ledger framing with less than 3 bytes for the report") - sequenceIdx = 0 - offset = 0 - result = struct.pack(">HBHH", channel, 0x05, sequenceIdx, len(command)) - sequenceIdx = sequenceIdx + 1 - if len(command) > packetSize - 7: - blockSize = packetSize - 7 - else: - blockSize = len(command) - result += command[offset : offset + blockSize] - offset = offset + blockSize - while offset != len(command): - result += struct.pack(">HBH", channel, 0x05, sequenceIdx) - sequenceIdx = sequenceIdx + 1 - if (len(command) - offset) > packetSize - 5: - blockSize = packetSize - 5 - else: - blockSize = len(command) - offset - result += command[offset : offset + blockSize] - offset = offset + blockSize - while (len(result) % packetSize) != 0: - result += b"\x00" - return bytearray(result) - -def unwrapResponseAPDU(channel, data, packetSize): - sequenceIdx = 0 - offset = 0 - if ((data is None) or (len(data) < 7 + 5)): - return None - if struct.unpack(">H", data[offset : offset + 2])[0] != channel: - raise BTChipException("Invalid channel") - offset += 2 - if data[offset] != 0x05: - raise BTChipException("Invalid tag") - offset += 1 - if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: - raise BTChipException("Invalid sequence") - offset += 2 - responseLength = struct.unpack(">H", data[offset : offset + 2])[0] - offset += 2 - if len(data) < 7 + responseLength: - return None - if responseLength > packetSize - 7: - blockSize = packetSize - 7 - else: - blockSize = responseLength - result = data[offset : offset + blockSize] - offset += blockSize - while (len(result) != responseLength): - sequenceIdx = sequenceIdx + 1 - if (offset == len(data)): - return None - if struct.unpack(">H", data[offset : offset + 2])[0] != channel: - raise BTChipException("Invalid channel") - offset += 2 - if data[offset] != 0x05: - raise BTChipException("Invalid tag") - offset += 1 - if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: - raise BTChipException("Invalid sequence") - offset += 2 - if (responseLength - len(result)) > packetSize - 5: - blockSize = packetSize - 5 - else: - blockSize = responseLength - len(result) - result += data[offset : offset + blockSize] - offset += blockSize - return bytearray(result) diff --git a/tests/btchippython/btchip/msqr.py b/tests/btchippython/btchip/msqr.py deleted file mode 100644 index f582ec26..00000000 --- a/tests/btchippython/btchip/msqr.py +++ /dev/null @@ -1,94 +0,0 @@ -# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/ - -def modular_sqrt(a, p): - """ Find a quadratic residue (mod p) of 'a'. p - must be an odd prime. - - Solve the congruence of the form: - x^2 = a (mod p) - And returns x. Note that p - x is also a root. - - 0 is returned is no square root exists for - these a and p. - - The Tonelli-Shanks algorithm is used (except - for some simple cases in which the solution - is known from an identity). This algorithm - runs in polynomial time (unless the - generalized Riemann hypothesis is false). - """ - # Simple cases - # - if legendre_symbol(a, p) != 1: - return 0 - elif a == 0: - return 0 - elif p == 2: - return p - elif p % 4 == 3: - return pow(a, (p + 1) / 4, p) - - # Partition p-1 to s * 2^e for an odd s (i.e. - # reduce all the powers of 2 from p-1) - # - s = p - 1 - e = 0 - while s % 2 == 0: - s /= 2 - e += 1 - - # Find some 'n' with a legendre symbol n|p = -1. - # Shouldn't take long. - # - n = 2 - while legendre_symbol(n, p) != -1: - n += 1 - - # Here be dragons! - # Read the paper "Square roots from 1; 24, 51, - # 10 to Dan Shanks" by Ezra Brown for more - # information - # - - # x is a guess of the square root that gets better - # with each iteration. - # b is the "fudge factor" - by how much we're off - # with the guess. The invariant x^2 = ab (mod p) - # is maintained throughout the loop. - # g is used for successive powers of n to update - # both a and b - # r is the exponent - decreases with each update - # - x = pow(a, (s + 1) / 2, p) - b = pow(a, s, p) - g = pow(n, s, p) - r = e - - while True: - t = b - m = 0 - for m in xrange(r): - if t == 1: - break - t = pow(t, 2, p) - - if m == 0: - return x - - gs = pow(g, 2 ** (r - m - 1), p) - g = (gs * gs) % p - x = (x * gs) % p - b = (b * g) % p - r = m - -def legendre_symbol(a, p): - """ Compute the Legendre symbol a|p using - Euler's criterion. p is a prime, a is - relatively prime to p (if p divides - a, then a|p = 0) - - Returns 1 if a has a square root modulo - p, -1 otherwise. - """ - ls = pow(a, (p - 1) / 2, p) - return -1 if ls == p - 1 else ls diff --git a/tests/btchippython/btchip/ui/__init__.py b/tests/btchippython/btchip/ui/__init__.py deleted file mode 100644 index 9b5894e7..00000000 --- a/tests/btchippython/btchip/ui/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - diff --git a/tests/btchippython/btchip/ui/personalization00start.py b/tests/btchippython/btchip/ui/personalization00start.py deleted file mode 100644 index 0429bee5..00000000 --- a/tests/btchippython/btchip/ui/personalization00start.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-00-start.ui' -# -# Created: Fri Sep 19 15:03:25 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 231) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(120, 20, 231, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(20, 60, 351, 61)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.NextButton = QtGui.QPushButton(Dialog) - self.NextButton.setGeometry(QtCore.QRect(310, 200, 75, 25)) - self.NextButton.setObjectName(_fromUtf8("NextButton")) - self.arningLabel = QtGui.QLabel(Dialog) - self.arningLabel.setGeometry(QtCore.QRect(20, 120, 351, 81)) - self.arningLabel.setWordWrap(True) - self.arningLabel.setObjectName(_fromUtf8("arningLabel")) - self.CancelButton = QtGui.QPushButton(Dialog) - self.CancelButton.setGeometry(QtCore.QRect(20, 200, 75, 25)) - self.CancelButton.setObjectName(_fromUtf8("CancelButton")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Your BTChip dongle is not set up - you\'ll be able to create a new wallet, or restore an existing one, and choose your security profile.", None, QtGui.QApplication.UnicodeUTF8)) - self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) - self.arningLabel.setText(QtGui.QApplication.translate("Dialog", "Sensitive information including your dongle PIN will be exchanged during this setup phase - it is recommended to execute it on a secure computer, disconnected from any network, especially if you restore a wallet backup.", None, QtGui.QApplication.UnicodeUTF8)) - self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalization01seed.py b/tests/btchippython/btchip/ui/personalization01seed.py deleted file mode 100644 index 60fa2aee..00000000 --- a/tests/btchippython/btchip/ui/personalization01seed.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-01-seed.ui' -# -# Created: Thu Aug 28 22:26:22 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 300) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(50, 20, 311, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(20, 60, 351, 61)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.NewWalletButton = QtGui.QRadioButton(Dialog) - self.NewWalletButton.setGeometry(QtCore.QRect(20, 130, 94, 21)) - self.NewWalletButton.setChecked(True) - self.NewWalletButton.setObjectName(_fromUtf8("NewWalletButton")) - self.buttonGroup = QtGui.QButtonGroup(Dialog) - self.buttonGroup.setObjectName(_fromUtf8("buttonGroup")) - self.buttonGroup.addButton(self.NewWalletButton) - self.RestoreWalletButton = QtGui.QRadioButton(Dialog) - self.RestoreWalletButton.setGeometry(QtCore.QRect(20, 180, 171, 21)) - self.RestoreWalletButton.setObjectName(_fromUtf8("RestoreWalletButton")) - self.buttonGroup.addButton(self.RestoreWalletButton) - self.seed = QtGui.QLineEdit(Dialog) - self.seed.setEnabled(False) - self.seed.setGeometry(QtCore.QRect(50, 210, 331, 21)) - self.seed.setEchoMode(QtGui.QLineEdit.Normal) - self.seed.setObjectName(_fromUtf8("seed")) - self.CancelButton = QtGui.QPushButton(Dialog) - self.CancelButton.setGeometry(QtCore.QRect(10, 270, 75, 25)) - self.CancelButton.setObjectName(_fromUtf8("CancelButton")) - self.NextButton = QtGui.QPushButton(Dialog) - self.NextButton.setGeometry(QtCore.QRect(320, 270, 75, 25)) - self.NextButton.setObjectName(_fromUtf8("NextButton")) - self.mnemonicNotAvailableLabel = QtGui.QLabel(Dialog) - self.mnemonicNotAvailableLabel.setGeometry(QtCore.QRect(130, 240, 171, 31)) - font = QtGui.QFont() - font.setItalic(True) - self.mnemonicNotAvailableLabel.setFont(font) - self.mnemonicNotAvailableLabel.setWordWrap(True) - self.mnemonicNotAvailableLabel.setObjectName(_fromUtf8("mnemonicNotAvailableLabel")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup - seed", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed (1/3)", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please select an option : either create a new wallet or restore an existing one", None, QtGui.QApplication.UnicodeUTF8)) - self.NewWalletButton.setText(QtGui.QApplication.translate("Dialog", "New Wallet", None, QtGui.QApplication.UnicodeUTF8)) - self.RestoreWalletButton.setText(QtGui.QApplication.translate("Dialog", "Restore wallet backup", None, QtGui.QApplication.UnicodeUTF8)) - self.seed.setPlaceholderText(QtGui.QApplication.translate("Dialog", "Enter an hexadecimal seed or a BIP 39 mnemonic code", None, QtGui.QApplication.UnicodeUTF8)) - self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) - self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) - self.mnemonicNotAvailableLabel.setText(QtGui.QApplication.translate("Dialog", "Mnemonic API is not available", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalization02security.py b/tests/btchippython/btchip/ui/personalization02security.py deleted file mode 100644 index f65c2896..00000000 --- a/tests/btchippython/btchip/ui/personalization02security.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-02-security.ui' -# -# Created: Thu Aug 28 22:26:22 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 503) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(20, 20, 361, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(10, 60, 351, 61)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.HardenedButton = QtGui.QRadioButton(Dialog) - self.HardenedButton.setGeometry(QtCore.QRect(20, 110, 81, 21)) - self.HardenedButton.setChecked(True) - self.HardenedButton.setObjectName(_fromUtf8("HardenedButton")) - self.buttonGroup = QtGui.QButtonGroup(Dialog) - self.buttonGroup.setObjectName(_fromUtf8("buttonGroup")) - self.buttonGroup.addButton(self.HardenedButton) - self.HardenedButton_2 = QtGui.QRadioButton(Dialog) - self.HardenedButton_2.setGeometry(QtCore.QRect(20, 210, 81, 21)) - self.HardenedButton_2.setObjectName(_fromUtf8("HardenedButton_2")) - self.buttonGroup.addButton(self.HardenedButton_2) - self.IntroLabel_2 = QtGui.QLabel(Dialog) - self.IntroLabel_2.setGeometry(QtCore.QRect(50, 140, 351, 61)) - self.IntroLabel_2.setWordWrap(True) - self.IntroLabel_2.setObjectName(_fromUtf8("IntroLabel_2")) - self.IntroLabel_3 = QtGui.QLabel(Dialog) - self.IntroLabel_3.setGeometry(QtCore.QRect(50, 230, 351, 61)) - self.IntroLabel_3.setWordWrap(True) - self.IntroLabel_3.setObjectName(_fromUtf8("IntroLabel_3")) - self.CancelButton = QtGui.QPushButton(Dialog) - self.CancelButton.setGeometry(QtCore.QRect(10, 470, 75, 25)) - self.CancelButton.setObjectName(_fromUtf8("CancelButton")) - self.NextButton = QtGui.QPushButton(Dialog) - self.NextButton.setGeometry(QtCore.QRect(310, 470, 75, 25)) - self.NextButton.setObjectName(_fromUtf8("NextButton")) - self.IntroLabel_4 = QtGui.QLabel(Dialog) - self.IntroLabel_4.setGeometry(QtCore.QRect(10, 300, 351, 61)) - self.IntroLabel_4.setWordWrap(True) - self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) - self.IntroLabel_5 = QtGui.QLabel(Dialog) - self.IntroLabel_5.setGeometry(QtCore.QRect(20, 380, 161, 31)) - self.IntroLabel_5.setWordWrap(True) - self.IntroLabel_5.setObjectName(_fromUtf8("IntroLabel_5")) - self.pin1 = QtGui.QLineEdit(Dialog) - self.pin1.setGeometry(QtCore.QRect(210, 380, 161, 21)) - self.pin1.setEchoMode(QtGui.QLineEdit.Password) - self.pin1.setObjectName(_fromUtf8("pin1")) - self.pin2 = QtGui.QLineEdit(Dialog) - self.pin2.setGeometry(QtCore.QRect(210, 420, 161, 21)) - self.pin2.setEchoMode(QtGui.QLineEdit.Password) - self.pin2.setObjectName(_fromUtf8("pin2")) - self.IntroLabel_6 = QtGui.QLabel(Dialog) - self.IntroLabel_6.setGeometry(QtCore.QRect(20, 420, 171, 31)) - self.IntroLabel_6.setWordWrap(True) - self.IntroLabel_6.setObjectName(_fromUtf8("IntroLabel_6")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup - security", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - security (2/3)", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please choose a security profile", None, QtGui.QApplication.UnicodeUTF8)) - self.HardenedButton.setText(QtGui.QApplication.translate("Dialog", "Hardened", None, QtGui.QApplication.UnicodeUTF8)) - self.HardenedButton_2.setText(QtGui.QApplication.translate("Dialog", "PIN only", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_2.setText(QtGui.QApplication.translate("Dialog", "You need to remove the dongle and insert it again to get a second factor validation of all operations. Recommended for expert users and to be fully protected against malwares.", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_3.setText(QtGui.QApplication.translate("Dialog", "You only need to enter a PIN once when inserting the dongle. Transactions are not protected against malwares", None, QtGui.QApplication.UnicodeUTF8)) - self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) - self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "Please choose a PIN associated to the BTChip dongle. The PIN protects the dongle in case it is stolen, and can be up to 32 characters. The dongle is wiped if a wrong PIN is entered 3 times in a row.", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_5.setText(QtGui.QApplication.translate("Dialog", "Enter the new PIN : ", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_6.setText(QtGui.QApplication.translate("Dialog", "Repeat the new PIN :", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalization03config.py b/tests/btchippython/btchip/ui/personalization03config.py deleted file mode 100644 index 02d96e5e..00000000 --- a/tests/btchippython/btchip/ui/personalization03config.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-03-config.ui' -# -# Created: Thu Aug 28 22:26:22 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 243) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(30, 10, 361, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(20, 50, 351, 61)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.qwertyButton = QtGui.QRadioButton(Dialog) - self.qwertyButton.setGeometry(QtCore.QRect(50, 110, 94, 21)) - self.qwertyButton.setChecked(True) - self.qwertyButton.setObjectName(_fromUtf8("qwertyButton")) - self.keyboardGroup = QtGui.QButtonGroup(Dialog) - self.keyboardGroup.setObjectName(_fromUtf8("keyboardGroup")) - self.keyboardGroup.addButton(self.qwertyButton) - self.qwertzButton = QtGui.QRadioButton(Dialog) - self.qwertzButton.setGeometry(QtCore.QRect(50, 140, 94, 21)) - self.qwertzButton.setObjectName(_fromUtf8("qwertzButton")) - self.keyboardGroup.addButton(self.qwertzButton) - self.azertyButton = QtGui.QRadioButton(Dialog) - self.azertyButton.setGeometry(QtCore.QRect(50, 170, 94, 21)) - self.azertyButton.setObjectName(_fromUtf8("azertyButton")) - self.keyboardGroup.addButton(self.azertyButton) - self.CancelButton = QtGui.QPushButton(Dialog) - self.CancelButton.setGeometry(QtCore.QRect(10, 210, 75, 25)) - self.CancelButton.setObjectName(_fromUtf8("CancelButton")) - self.NextButton = QtGui.QPushButton(Dialog) - self.NextButton.setGeometry(QtCore.QRect(320, 210, 75, 25)) - self.NextButton.setObjectName(_fromUtf8("NextButton")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - config (3/3)", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please select your keyboard type to type the second factor confirmation", None, QtGui.QApplication.UnicodeUTF8)) - self.qwertyButton.setText(QtGui.QApplication.translate("Dialog", "QWERTY", None, QtGui.QApplication.UnicodeUTF8)) - self.qwertzButton.setText(QtGui.QApplication.translate("Dialog", "QWERTZ", None, QtGui.QApplication.UnicodeUTF8)) - self.azertyButton.setText(QtGui.QApplication.translate("Dialog", "AZERTY", None, QtGui.QApplication.UnicodeUTF8)) - self.CancelButton.setText(QtGui.QApplication.translate("Dialog", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) - self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalization04finalize.py b/tests/btchippython/btchip/ui/personalization04finalize.py deleted file mode 100644 index 00f20d39..00000000 --- a/tests/btchippython/btchip/ui/personalization04finalize.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-04-finalize.ui' -# -# Created: Thu Aug 28 22:26:22 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 267) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(20, 20, 361, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.FinishButton = QtGui.QPushButton(Dialog) - self.FinishButton.setGeometry(QtCore.QRect(320, 230, 75, 25)) - self.FinishButton.setObjectName(_fromUtf8("FinishButton")) - self.IntroLabel_4 = QtGui.QLabel(Dialog) - self.IntroLabel_4.setGeometry(QtCore.QRect(10, 70, 351, 61)) - self.IntroLabel_4.setWordWrap(True) - self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) - self.IntroLabel_5 = QtGui.QLabel(Dialog) - self.IntroLabel_5.setGeometry(QtCore.QRect(50, 140, 121, 21)) - self.IntroLabel_5.setWordWrap(True) - self.IntroLabel_5.setObjectName(_fromUtf8("IntroLabel_5")) - self.pin1 = QtGui.QLineEdit(Dialog) - self.pin1.setGeometry(QtCore.QRect(200, 140, 181, 21)) - self.pin1.setEchoMode(QtGui.QLineEdit.Password) - self.pin1.setObjectName(_fromUtf8("pin1")) - self.remainingAttemptsLabel = QtGui.QLabel(Dialog) - self.remainingAttemptsLabel.setGeometry(QtCore.QRect(120, 170, 171, 31)) - font = QtGui.QFont() - font.setItalic(True) - self.remainingAttemptsLabel.setFont(font) - self.remainingAttemptsLabel.setWordWrap(True) - self.remainingAttemptsLabel.setObjectName(_fromUtf8("remainingAttemptsLabel")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup - security", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - completed", None, QtGui.QApplication.UnicodeUTF8)) - self.FinishButton.setText(QtGui.QApplication.translate("Dialog", "Finish", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "BTChip setup is completed. Please enter your PIN to validate it then press Finish", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_5.setText(QtGui.QApplication.translate("Dialog", "BTChip PIN :", None, QtGui.QApplication.UnicodeUTF8)) - self.remainingAttemptsLabel.setText(QtGui.QApplication.translate("Dialog", "Remaining attempts", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup01.py b/tests/btchippython/btchip/ui/personalizationseedbackup01.py deleted file mode 100644 index fb1ccbe8..00000000 --- a/tests/btchippython/btchip/ui/personalizationseedbackup01.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-seedbackup-01.ui' -# -# Created: Thu Aug 28 22:26:22 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 300) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(30, 20, 351, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.NextButton = QtGui.QPushButton(Dialog) - self.NextButton.setGeometry(QtCore.QRect(320, 270, 75, 25)) - self.NextButton.setObjectName(_fromUtf8("NextButton")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(10, 100, 351, 31)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.IntroLabel_2 = QtGui.QLabel(Dialog) - self.IntroLabel_2.setGeometry(QtCore.QRect(10, 140, 351, 31)) - self.IntroLabel_2.setWordWrap(True) - self.IntroLabel_2.setObjectName(_fromUtf8("IntroLabel_2")) - self.IntroLabel_3 = QtGui.QLabel(Dialog) - self.IntroLabel_3.setGeometry(QtCore.QRect(10, 180, 351, 41)) - self.IntroLabel_3.setWordWrap(True) - self.IntroLabel_3.setObjectName(_fromUtf8("IntroLabel_3")) - self.TitleLabel_2 = QtGui.QLabel(Dialog) - self.TitleLabel_2.setGeometry(QtCore.QRect(90, 60, 251, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel_2.setFont(font) - self.TitleLabel_2.setObjectName(_fromUtf8("TitleLabel_2")) - self.IntroLabel_4 = QtGui.QLabel(Dialog) - self.IntroLabel_4.setGeometry(QtCore.QRect(10, 220, 351, 41)) - self.IntroLabel_4.setWordWrap(True) - self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) - self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "A new seed has been generated for your wallet.", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_2.setText(QtGui.QApplication.translate("Dialog", "You must backup this seed and keep it out of reach of hackers (typically by keeping it on paper).", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_3.setText(QtGui.QApplication.translate("Dialog", "You can use this seed to restore your dongle if you lose it or access your funds with any other compatible wallet.", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel_2.setText(QtGui.QApplication.translate("Dialog", "READ CAREFULLY", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "Press Next to start the backuping process.", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup02.py b/tests/btchippython/btchip/ui/personalizationseedbackup02.py deleted file mode 100644 index 164aa9c3..00000000 --- a/tests/btchippython/btchip/ui/personalizationseedbackup02.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-seedbackup-02.ui' -# -# Created: Thu Aug 28 22:26:22 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 300) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(30, 20, 351, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(20, 70, 351, 31)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.NextButton = QtGui.QPushButton(Dialog) - self.NextButton.setGeometry(QtCore.QRect(320, 270, 75, 25)) - self.NextButton.setObjectName(_fromUtf8("NextButton")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Please disconnect the dongle then press Next", None, QtGui.QApplication.UnicodeUTF8)) - self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup03.py b/tests/btchippython/btchip/ui/personalizationseedbackup03.py deleted file mode 100644 index bb106898..00000000 --- a/tests/btchippython/btchip/ui/personalizationseedbackup03.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-seedbackup-03.ui' -# -# Created: Thu Aug 28 22:26:22 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(400, 513) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(20, 10, 351, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(20, 50, 351, 61)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.IntroLabel_2 = QtGui.QLabel(Dialog) - self.IntroLabel_2.setGeometry(QtCore.QRect(20, 120, 351, 31)) - self.IntroLabel_2.setWordWrap(True) - self.IntroLabel_2.setObjectName(_fromUtf8("IntroLabel_2")) - self.IntroLabel_3 = QtGui.QLabel(Dialog) - self.IntroLabel_3.setGeometry(QtCore.QRect(20, 160, 351, 51)) - self.IntroLabel_3.setWordWrap(True) - self.IntroLabel_3.setObjectName(_fromUtf8("IntroLabel_3")) - self.IntroLabel_4 = QtGui.QLabel(Dialog) - self.IntroLabel_4.setGeometry(QtCore.QRect(20, 220, 351, 51)) - self.IntroLabel_4.setWordWrap(True) - self.IntroLabel_4.setObjectName(_fromUtf8("IntroLabel_4")) - self.IntroLabel_5 = QtGui.QLabel(Dialog) - self.IntroLabel_5.setGeometry(QtCore.QRect(20, 280, 351, 71)) - self.IntroLabel_5.setWordWrap(True) - self.IntroLabel_5.setObjectName(_fromUtf8("IntroLabel_5")) - self.IntroLabel_6 = QtGui.QLabel(Dialog) - self.IntroLabel_6.setGeometry(QtCore.QRect(20, 350, 351, 51)) - self.IntroLabel_6.setWordWrap(True) - self.IntroLabel_6.setObjectName(_fromUtf8("IntroLabel_6")) - self.IntroLabel_7 = QtGui.QLabel(Dialog) - self.IntroLabel_7.setGeometry(QtCore.QRect(20, 410, 351, 51)) - self.IntroLabel_7.setWordWrap(True) - self.IntroLabel_7.setObjectName(_fromUtf8("IntroLabel_7")) - self.NextButton = QtGui.QPushButton(Dialog) - self.NextButton.setGeometry(QtCore.QRect(310, 480, 75, 25)) - self.NextButton.setObjectName(_fromUtf8("NextButton")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "If you do not trust this computer, perform the following steps on a trusted one or a different device. Anything supporting keyboard input will work (smartphone, TV box ...)", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_2.setText(QtGui.QApplication.translate("Dialog", "Open a text editor, set the focus on the text editor, then insert the dongle", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_3.setText(QtGui.QApplication.translate("Dialog", "After a very short time, the dongle will type the seed as hexadecimal (0..9 A..F) characters, starting with \"seed\" and ending with \"X\"", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_4.setText(QtGui.QApplication.translate("Dialog", "If you perform those steps on Windows, a new device driver will be loaded the first time and the seed will not be typed. This is normal.", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_5.setText(QtGui.QApplication.translate("Dialog", "If you perform those steps on Mac, you\'ll get a popup asking you to select a keyboard type the first time and the seed will not be typed. This is normal, just close the popup.", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_6.setText(QtGui.QApplication.translate("Dialog", "If you did not see the seed for any reason, keep the focus on the text editor, unplug and plug the dongle again twice.", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel_7.setText(QtGui.QApplication.translate("Dialog", "Then press Next once you wrote the seed to a safe medium (i.e. paper) and unplugged the dongle", None, QtGui.QApplication.UnicodeUTF8)) - self.NextButton.setText(QtGui.QApplication.translate("Dialog", "Next", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/btchip/ui/personalizationseedbackup04.py b/tests/btchippython/btchip/ui/personalizationseedbackup04.py deleted file mode 100644 index e2db35f0..00000000 --- a/tests/btchippython/btchip/ui/personalizationseedbackup04.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'personalization-seedbackup-04.ui' -# -# Created: Thu Aug 28 22:26:23 2014 -# by: PyQt4 UI code generator 4.9.1 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName(_fromUtf8("Dialog")) - Dialog.resize(554, 190) - self.TitleLabel = QtGui.QLabel(Dialog) - self.TitleLabel.setGeometry(QtCore.QRect(30, 10, 351, 31)) - font = QtGui.QFont() - font.setPointSize(20) - font.setBold(True) - font.setItalic(True) - font.setWeight(75) - self.TitleLabel.setFont(font) - self.TitleLabel.setObjectName(_fromUtf8("TitleLabel")) - self.IntroLabel = QtGui.QLabel(Dialog) - self.IntroLabel.setGeometry(QtCore.QRect(10, 50, 351, 51)) - self.IntroLabel.setWordWrap(True) - self.IntroLabel.setObjectName(_fromUtf8("IntroLabel")) - self.seedOkButton = QtGui.QPushButton(Dialog) - self.seedOkButton.setGeometry(QtCore.QRect(20, 140, 501, 25)) - self.seedOkButton.setObjectName(_fromUtf8("seedOkButton")) - self.seedKoButton = QtGui.QPushButton(Dialog) - self.seedKoButton.setGeometry(QtCore.QRect(20, 110, 501, 25)) - self.seedKoButton.setObjectName(_fromUtf8("seedKoButton")) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "BTChip setup", None, QtGui.QApplication.UnicodeUTF8)) - self.TitleLabel.setText(QtGui.QApplication.translate("Dialog", "BTChip setup - seed backup", None, QtGui.QApplication.UnicodeUTF8)) - self.IntroLabel.setText(QtGui.QApplication.translate("Dialog", "Did you see the seed correctly displayed and did you backup it properly ?", None, QtGui.QApplication.UnicodeUTF8)) - self.seedOkButton.setText(QtGui.QApplication.translate("Dialog", "Yes, the seed is backed up properly and kept in a safe place, move on", None, QtGui.QApplication.UnicodeUTF8)) - self.seedKoButton.setText(QtGui.QApplication.translate("Dialog", "No, I didn\'t see the seed. Wipe the dongle and start over", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/tests/btchippython/samples/getFirmwareVersion.py b/tests/btchippython/samples/getFirmwareVersion.py deleted file mode 100644 index 57ffcd38..00000000 --- a/tests/btchippython/samples/getFirmwareVersion.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from btchip.btchip import * -import sys - -dongle = getDongle(True) -app = btchip(dongle) -print(app.getFirmwareVersion()['version']) - diff --git a/tests/btchippython/samples/runScript.py b/tests/btchippython/samples/runScript.py deleted file mode 100644 index d3b283b1..00000000 --- a/tests/btchippython/samples/runScript.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from btchip.btchip import * -import sys -import binascii - -if len(sys.argv) < 2: - print("Usage : %s script to run" % sys.argv[0]) - sys.exit(2) - -dongle = getDongle(True) - -scriptFile = open(sys.argv[1], "r") -line = scriptFile.readline() -while line: - if (len(line) == 0) or (line[0] == '#') or (line.find('[') >= 0) or (line.find(']') >= 0): - line = scriptFile.readline() - continue - line = line.replace('\"', '') - line = line.replace(',', '') - cancelResponse = (line[0] == '!') - timeout = 10000 - if cancelResponse: - line = line[1:] - timeout = 1 - try: - line = line.strip() - if len(line) == 0: - continue - dongle.exchange(bytearray(binascii.unhexlify(line)), timeout) - except Exception: - if cancelResponse: - pass - else: - raise - line = scriptFile.readline() -scriptFile.close() diff --git a/tests/btchippython/setup.py b/tests/btchippython/setup.py deleted file mode 100644 index ff0e8995..00000000 --- a/tests/btchippython/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -#from distribute_setup import use_setuptools -#use_setuptools() - -from setuptools import setup, find_packages -from os.path import dirname, join - -here = dirname(__file__) -import btchip -setup( - name='btchippython', - version=btchip.__version__, - author='BTChip', - author_email='hello@ledger.fr', - description='Python library to communicate with Ledger Nano dongle', - long_description=open(join(here, 'README.md')).read(), - url='https://github.com/LedgerHQ/btchip-python', - packages=find_packages(), - install_requires=['hidapi>=0.7.99', 'ecdsa>=0.9'], - extras_require = { - 'smartcard': [ 'python-pyscard>=1.6.12-4build1' ] - }, - include_package_data=True, - zip_safe=False, - classifiers=[ - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: MacOS :: MacOS X' - ] -) - diff --git a/tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js b/tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js deleted file mode 100644 index f825c5a7..00000000 --- a/tests/btchippython/tests/coinkite-cosigning/testGetPublicKeyDev.js +++ /dev/null @@ -1,115 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -# Coinkite co-signer provisioning -# To be run with the dongle in developer mode, PIN verified - -import hashlib -from btchip.btchip import * -from btchip.btchipUtils import * -from base64 import b64encode - -# Replace with your own seed (preferably import it and store it), key path, and Testnet flag -SEED = bytearray("fe721b95503a18a14d93914e02ff153f924737c336b01f98f2ff39395f630187".decode('hex')) -KEYPATH = "0'/2/0" -TESTNET = True - -# From Electrum - -__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -__b58base = len(__b58chars) - -def b58encode(v): - """ encode v, which is a string of bytes, to base58.""" - - long_value = 0L - for (i, c) in enumerate(v[::-1]): - long_value += (256**i) * ord(c) - - result = '' - while long_value >= __b58base: - div, mod = divmod(long_value, __b58base) - result = __b58chars[mod] + result - long_value = div - result = __b58chars[long_value] + result - - # Bitcoin does a little leading-zero-compression: - # leading 0-bytes in the input become leading-1s - nPad = 0 - for c in v: - if c == '\0': nPad += 1 - else: break - - return (__b58chars[0]*nPad) + result - -def EncodeBase58Check(vchIn): - hash = Hash(vchIn) - return b58encode(vchIn + hash[0:4]) - -def sha256(x): - return hashlib.sha256(x).digest() - - -def Hash(x): - if type(x) is unicode: x=x.encode('utf-8') - return sha256(sha256(x)) - -def i4b(self, x): - return pack('>I', x) - - -# /from Electrum - -def getXpub(publicKeyData, testnet=False): - header = ("043587CF" if testnet else "0488B21E") - result = header.decode('hex') + chr(publicKeyData['depth']) + str(publicKeyData['parentFingerprint']) + str(publicKeyData['childNumber']) + str(publicKeyData['chainCode']) + str(compress_public_key(publicKeyData['publicKey'])) - return EncodeBase58Check(result) - -def signMessage(encodedPrivateKey, data): - messageData = bytearray("\x18Bitcoin Signed Message:\n") - writeVarint(len(data), messageData) - messageData.extend(data) - messageHash = Hash(messageData) - signature = app.signImmediate(encodedPrivateKey, messageHash) - - # Parse the ASN.1 signature - - rLength = signature[3] - r = signature[4 : 4 + rLength] - sLength = signature[4 + rLength + 1] - s = signature[4 + rLength + 2:] - if rLength == 33: - r = r[1:] - if sLength == 33: - s = s[1:] - r = str(r) - s = str(s) - - # And convert it - - return b64encode(chr(27 + 4 + (signature[0] & 0x01)) + r + s) - -dongle = getDongle(True) -app = btchip(dongle) -seed = app.importPrivateKey(SEED, TESTNET) -privateKey = app.deriveBip32Key(seed, KEYPATH) -publicKeyData = app.getPublicKey(privateKey) -print getXpub(publicKeyData, TESTNET) -print signMessage(privateKey, "Coinkite") -dongle.close() diff --git a/tests/btchippython/tests/coinkite-cosigning/testSignDev.js b/tests/btchippython/tests/coinkite-cosigning/testSignDev.js deleted file mode 100644 index 03cd78ef..00000000 --- a/tests/btchippython/tests/coinkite-cosigning/testSignDev.js +++ /dev/null @@ -1,168 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -# Coinkite co-signer signing -# To be run with the dongle in developer mode, PIN verified -# Pass the JSON request to sign as parameter - -# TODO : verify Coinkite signature in the request - -import hashlib -import sys -import json -from btchip.btchip import * -from btchip.btchipUtils import * -from base64 import b64encode - -# Replace with your own seed (preferably import it and store it), key path, and Testnet flag -SEED = bytearray("fe721b95503a18a14d93914e02ff153f924737c336b01f98f2ff39395f630187".decode('hex')) -KEYPATH = "0'/2/0" -TESTNET = True - - -# From Electrum - -__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -__b58base = len(__b58chars) - -def b58encode(v): - """ encode v, which is a string of bytes, to base58.""" - - long_value = 0L - for (i, c) in enumerate(v[::-1]): - long_value += (256**i) * ord(c) - - result = '' - while long_value >= __b58base: - div, mod = divmod(long_value, __b58base) - result = __b58chars[mod] + result - long_value = div - result = __b58chars[long_value] + result - - # Bitcoin does a little leading-zero-compression: - # leading 0-bytes in the input become leading-1s - nPad = 0 - for c in v: - if c == '\0': nPad += 1 - else: break - - return (__b58chars[0]*nPad) + result - -def EncodeBase58Check(vchIn): - hash = Hash(vchIn) - return b58encode(vchIn + hash[0:4]) - -def sha256(x): - return hashlib.sha256(x).digest() - -def hash_160(public_key): - try: - md = hashlib.new('ripemd160') - md.update(sha256(public_key)) - return md.digest() - except Exception: - import ripemd - md = ripemd.new(sha256(public_key)) - return md.digest() - -def Hash(x): - if type(x) is unicode: x=x.encode('utf-8') - return sha256(sha256(x)) - -def i4b(self, x): - return pack('>I', x) - - -# /from Electrum - -def getXpub(publicKeyData, testnet=False): - header = ("043587CF" if testnet else "0488B21E") - result = header.decode('hex') + chr(publicKeyData['depth']) + str(publicKeyData['parentFingerprint']) + str(publicKeyData['childNumber']) + str(publicKeyData['chainCode']) + str(compress_public_key(publicKeyData['publicKey'])) - return EncodeBase58Check(result) - -def getPub(publicKeyData, testnet=False): - header = ("6F" if testnet else "00") - keyData = hash_160(str(compress_public_key(publicKeyData))) - return EncodeBase58Check(header.decode('hex') + keyData) - -def signMessage(encodedPrivateKey, data): - messageData = bytearray("\x18Bitcoin Signed Message:\n") - writeVarint(len(data), messageData) - messageData.extend(data) - messageHash = Hash(messageData) - signature = app.signImmediate(encodedPrivateKey, messageHash) - - # Parse the ASN.1 signature - - rLength = signature[3] - r = signature[4 : 4 + rLength] - sLength = signature[4 + rLength + 1] - s = signature[4 + rLength + 2:] - if rLength == 33: - r = r[1:] - if sLength == 33: - s = s[1:] - r = str(r) - s = str(s) - - # And convert it - - return b64encode(chr(27 + 4 + (signature[0] & 0x01)) + r + s) - - -f = open(sys.argv[1], 'r') -signData = json.load(f) -requestData = json.loads(signData['contents']) - -result = {} -result['cosigner'] = requestData['cosigner'] -result['request'] = requestData['request'] -result['signatures'] = [] - -dongle = getDongle(True) -app = btchip(dongle) -seed = app.importPrivateKey(SEED, TESTNET) -privateKey = app.deriveBip32Key(seed, KEYPATH) -publicKeyData = app.getPublicKey(privateKey) - -wallets = {} -for key in requestData['req_keys'].keys(): - privateKeyDiv = app.deriveBip32Key(privateKey, key) - publicKeyDataDiv = app.getPublicKey(privateKeyDiv) - if getPub(publicKeyDataDiv['publicKey'], TESTNET) == requestData['req_keys'][key][0]: - wallets[key] = privateKeyDiv - else: - raise "Invalid wallet, could not match key" - -for signInput in requestData['inputs']: - sigHash = signInput[1].decode('hex') - signature = "\x30" + str(app.signImmediate(wallets[signInput[0]], sigHash)[1:]) - signature = signature + "\x01" - result['signatures'].append([signature.encode('hex'), sigHash.encode('hex'), signInput[0]]) - -body = {} -body['_humans'] = "Upload this set of signatures to Coinkite." -body['content'] = json.dumps(result) -body['signature'] = signMessage(privateKey, body['content']) -body['signed_by'] = getPub(publicKeyData['publicKey'], False) # Bug, should use network - -print json.dumps(body) - -dongle.close() - diff --git a/tests/btchippython/tests/testConnectivity.py b/tests/btchippython/tests/testConnectivity.py deleted file mode 100644 index 85ab29f9..00000000 --- a/tests/btchippython/tests/testConnectivity.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from btchip.btchip import * -#from btchip.btchipUtils import * - -# Run on any dongle to test connectivity. - -dongle = getDongle(True) -app = btchip(dongle) - -print('btchip firmware version:') -print(app.getFirmwareVersion()) - -print('some random number from the dongle:') -print(map(hex, app.getRandom(20))) - -dongle.close() diff --git a/tests/btchippython/tests/testMessageSignature.py b/tests/btchippython/tests/testMessageSignature.py deleted file mode 100644 index 7d041ed1..00000000 --- a/tests/btchippython/tests/testMessageSignature.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from btchip.btchip import * -from btchip.btchipUtils import * - -# Run on non configured dongle or dongle configured with test seed below - -SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284E88F10C57CF569BB3DADF7B1027F2D".decode('hex')) - -MESSAGE = "Campagne de Sarkozy : une double comptabilite chez Bygmalion" - -SECONDFACTOR_1 = "Powercycle then confirm signature of .Campagne de Sarkozy : une double comptabilite chez Bygmalion. for address 17JusYNVXLPm3hBPzzRQkARYDMUBgRUMVc with PIN" -SIGNATURE = bytearray("30450221009a0d28391c0535aec1077bbb86614c8f3c384a3e9aa1a124bfb9ce9649196b7e02200efa1adc010a7bdde4784ee98441e402f93b3c50a2760cb09dda07501e02c81f".decode('hex')) - -# Optional setup -dongle = getDongle(True) -app = btchip(dongle) -try: - app.setup(btchip.OPERATION_MODE_WALLET, btchip.FEATURE_RFC6979, 0x00, 0x05, "1234", None, btchip.QWERTY_KEYMAP, SEED) -except Exception: - pass -# Authenticate -app.verifyPin("1234") -# Start signing -app.signMessagePrepare("0'/0/0", MESSAGE) -dongle.close() -# Wait for the second factor confirmation -# Done on the same application for test purposes, this is typically done in another window -# or another computer for bigger transactions -response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") -if not response.startswith(SECONDFACTOR_1): - raise BTChipException("Invalid second factor") -# Get a reference to the dongle again, as it was disconnected -dongle = getDongle(True) -app = btchip(dongle) -# Compute the signature -signature = app.signMessageSign(response[len(response) - 4:]) -if signature <> SIGNATURE: - raise BTChipException("Invalid signature") diff --git a/tests/btchippython/tests/testMultisigArmory.py b/tests/btchippython/tests/testMultisigArmory.py deleted file mode 100644 index c68bfb53..00000000 --- a/tests/btchippython/tests/testMultisigArmory.py +++ /dev/null @@ -1,194 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from btchip.btchip import * -from btchip.btchipUtils import * -import json - -""" -Signs a TX generated by Armory. That TX: - -{ - 'inputs': [{ - 'p2shscript': '52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae', - 'supporttxhash_be': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', - 'sequence': 4294967295, - 'keys': [{ - 'dersighex': '', - 'pubkeyhex': '0269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c6283', - 'wltlochex': '' - }, { - 'dersighex': '', - 'pubkeyhex': '02f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb5', - 'wltlochex': '' - }, { - 'dersighex': '', - 'pubkeyhex': '037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b9102', - 'wltlochex': '' - }], - 'contriblabel': u '', - 'supporttxhash_le': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c', - 'contribid': 'JLBercZk', - 'version': 1, - 'inputvalue': 46000000, - 'outpoint': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000', - 'magicbytes': '0b110907', - 'supporttx': '01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000', - 'numkeys': 3, - 'supporttxhash': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', - 'supporttxoutindex': 0 - }], - 'fee': 10000, - 'locktimeint': 0, - 'outputs': [{ - 'txoutvalue': 10000000, - 'authdata': '', - 'contriblabel': '', - 'p2shscript': '', - 'scripttypeint': 4, - 'isp2sh': True, - 'txoutscript': 'a914c0c3b6ada732c797881d00de6c350eec96e3d22287', - 'authmethod': 'NONE', - 'hasaddrstr': True, - 'contribid': '', - 'version': 1, - 'ismultisig': False, - 'magicbytes': '0b110907', - 'addrstr': '2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8', - 'scripttypestr': 'Standard (P2SH)', - 'wltlocator': '' - }, { - 'txoutvalue': 35990000, - 'authdata': '', - 'contriblabel': '', - 'p2shscript': '', - 'scripttypeint': 4, - 'isp2sh': True, - 'txoutscript': 'a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387', - 'authmethod': 'NONE', - 'hasaddrstr': True, - 'contribid': '', - 'version': 1, - 'ismultisig': False, - 'magicbytes': '0b110907', - 'addrstr': '2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1', - 'scripttypestr': 'Standard (P2SH)', - 'wltlocator': '' - }], - 'sumoutputs': 45990000, - 'suminputs': 46000000, - 'version': 1, - 'numoutputs': 2, - 'magicbytes': '0b110907', - 'locktimedate': '', - 'locktimeblock': 0, - 'id': '8jkccikU', - 'numinputs': 1 -} - -Input comes from vout[0] of 0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300. -TX I want to generate is 0.10 to 2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8 - -The multisig address 2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1 contains 0.46 BTC, and is generated -using the public keys 0'/0/0, 0'/0/1, and 0'/0/2 from the seed below. - -""" - -# Run on non configured dongle or dongle configured with test seed below - -SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284C88A10C57CF569BB3DADF7B1027F2D".decode('hex')) - -# Armory supporttx -UTX = bytearray("01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000".decode('hex')) -UTXO_INDEX = 0 -OUTPUT = bytearray("02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex')) -# Armory p2shscript -REDEEMSCRIPT = bytearray("52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae".decode('hex')) - -SIGNATURE_0 = bytearray("3044022056cb1b781fd04cfe6c04756ad56d02e5512f3fe7f411bc22d1594da5c815a393022074ad7f4d47af7c3f8a7ddf0ba2903f986a88649b0018ce1538c379b304a6a23801".decode('hex')) -SIGNATURE_1 = bytearray("304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a5026001".decode('hex')) -SIGNATURE_2 = bytearray("30440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db01".decode('hex')) -TRANSACTION = bytearray("0100000001008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000fc004730440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db0147304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a50260014c6952210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253aeffffffff02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c38700000000".decode('hex')) - -SECONDFACTOR_1 = "RELAXED MODE Powercycle then confirm use of 0.46 BTC with PIN" - -# Armory txoutscript -output = get_output_script([["0.1", bytearray("a914c0c3b6ada732c797881d00de6c350eec96e3d22287".decode('hex'))], ["0.3599", bytearray("a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex'))]]); -if output<>OUTPUT: - raise BTChipException("Invalid output script encoding"); - -# Optional setup -dongle = getDongle(True) -app = btchip(dongle) -try: - app.setup(btchip.OPERATION_MODE_RELAXED_WALLET, btchip.FEATURE_RFC6979, 111, 196, "1234", None, btchip.QWERTY_KEYMAP, SEED) -except Exception: - pass -# Authenticate -app.verifyPin("1234") -# Get the trusted input associated to the UTXO -transaction = bitcoinTransaction(UTX) -print transaction -trustedInput = app.getTrustedInput(transaction, UTXO_INDEX) -# Start composing the transaction -app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) -app.finalizeInputFull(OUTPUT) -dongle.close() -# Wait for the second factor confirmation -# Done on the same application for test purposes, this is typically done in another window -# or another computer for bigger transactions -response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") -if not response.startswith(SECONDFACTOR_1): - raise BTChipException("Invalid second factor") -# Get a reference to the dongle again, as it was disconnected -dongle = getDongle(True) -app = btchip(dongle) -# Replay the transaction, this time continue it since the second factor is ready -app.startUntrustedTransaction(False, 0, [trustedInput], REDEEMSCRIPT) -app.finalizeInputFull(OUTPUT) -# Provide the second factor to finalize the signature -signature1 = app.untrustedHashSign("0'/0/1", response[len(response) - 4:]) -if signature1 <> SIGNATURE_1: - raise BTChipException("Invalid signature1") - -# Same thing for the second signature - -app.verifyPin("1234") -app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) -app.finalizeInputFull(OUTPUT) -dongle.close() -response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") -if not response.startswith(SECONDFACTOR_1): - raise BTChipException("Invalid second factor") -dongle = getDongle(True) -app = btchip(dongle) -app.startUntrustedTransaction(False, 0, [trustedInput], REDEEMSCRIPT) -app.finalizeInputFull(OUTPUT) -signature2 = app.untrustedHashSign("0'/0/2", response[len(response) - 4:]) -if signature2 <> SIGNATURE_2: - raise BTChipException("Invalid signature2") - -# Finalize the transaction - build the redeem script and put everything together -inputScript = get_p2sh_input_script(REDEEMSCRIPT, [signature2, signature1]) -transaction = format_transaction(OUTPUT, [ [ trustedInput['value'], inputScript] ]) -print "Generated transaction : " + str(transaction).encode('hex') -if transaction <> TRANSACTION: - raise BTChipException("Invalid transaction") -# The transaction is ready to be broadcast, enjoy - diff --git a/tests/btchippython/tests/testMultisigArmoryNo2FA.py b/tests/btchippython/tests/testMultisigArmoryNo2FA.py deleted file mode 100644 index ff7c5b25..00000000 --- a/tests/btchippython/tests/testMultisigArmoryNo2FA.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from btchip.btchip import * -from btchip.btchipUtils import * -import json - -""" -Signs a TX generated by Armory. That TX: - -{ - 'inputs': [{ - 'p2shscript': '52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae', - 'supporttxhash_be': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', - 'sequence': 4294967295, - 'keys': [{ - 'dersighex': '', - 'pubkeyhex': '0269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c6283', - 'wltlochex': '' - }, { - 'dersighex': '', - 'pubkeyhex': '02f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb5', - 'wltlochex': '' - }, { - 'dersighex': '', - 'pubkeyhex': '037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b9102', - 'wltlochex': '' - }], - 'contriblabel': u '', - 'supporttxhash_le': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c', - 'contribid': 'JLBercZk', - 'version': 1, - 'inputvalue': 46000000, - 'outpoint': '008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000', - 'magicbytes': '0b110907', - 'supporttx': '01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000', - 'numkeys': 3, - 'supporttxhash': '0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300', - 'supporttxoutindex': 0 - }], - 'fee': 10000, - 'locktimeint': 0, - 'outputs': [{ - 'txoutvalue': 10000000, - 'authdata': '', - 'contriblabel': '', - 'p2shscript': '', - 'scripttypeint': 4, - 'isp2sh': True, - 'txoutscript': 'a914c0c3b6ada732c797881d00de6c350eec96e3d22287', - 'authmethod': 'NONE', - 'hasaddrstr': True, - 'contribid': '', - 'version': 1, - 'ismultisig': False, - 'magicbytes': '0b110907', - 'addrstr': '2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8', - 'scripttypestr': 'Standard (P2SH)', - 'wltlocator': '' - }, { - 'txoutvalue': 35990000, - 'authdata': '', - 'contriblabel': '', - 'p2shscript': '', - 'scripttypeint': 4, - 'isp2sh': True, - 'txoutscript': 'a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387', - 'authmethod': 'NONE', - 'hasaddrstr': True, - 'contribid': '', - 'version': 1, - 'ismultisig': False, - 'magicbytes': '0b110907', - 'addrstr': '2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1', - 'scripttypestr': 'Standard (P2SH)', - 'wltlocator': '' - }], - 'sumoutputs': 45990000, - 'suminputs': 46000000, - 'version': 1, - 'numoutputs': 2, - 'magicbytes': '0b110907', - 'locktimedate': '', - 'locktimeblock': 0, - 'id': '8jkccikU', - 'numinputs': 1 -} - -Input comes from vout[0] of 0c1676b8fc1adaca53221290e242b8eb80fd6b89aa83f2fa0106f87e13388300. -TX I want to generate is 0.10 to 2NApUBXv4NB8pm834pHUajiUL6rvFaaj6N8 - -The multisig address 2NDuYxRrmAs2fRcMj4ew2F41aFp2PN9yiV1 contains 0.46 BTC, and is generated -using the public keys 0'/0/0, 0'/0/1, and 0'/0/2 from the seed below. - -""" - -# Run on non configured dongle or dongle configured with test seed below - -SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284C88A10C57CF569BB3DADF7B1027F2D".decode('hex')) - -UTX = bytearray("01000000013e9fe12917d854a0e093b982eaa46990289e2262f2db9fc1bd3f13718f3c806e010000006b483045022100af668e482e3ed363f51b36ddabad7cdf20d177104c92b8676a5b14f51107179602206c4ecd67544c74c6689ca453e2157d0c0b8a4608d85956429d2615275a51c66f01210374db359a004626daf2fcf10b8601f5f39438848a6733c768e88ce0ad398ae79dffffffff0280e7bd020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c3876c8fc846000000001976a914af58f09cf65b213bb9bd181a94e133b4ad4d6b2788ac00000000".decode('hex')) -UTXO_INDEX = 0 -OUTPUT = bytearray("02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex')) -# Armory p2shscript -REDEEMSCRIPT = bytearray("52210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253ae".decode('hex')) - -SIGNATURE_0 = bytearray("3044022056cb1b781fd04cfe6c04756ad56d02e5512f3fe7f411bc22d1594da5c815a393022074ad7f4d47af7c3f8a7ddf0ba2903f986a88649b0018ce1538c379b304a6a23801".decode('hex')) -SIGNATURE_1 = bytearray("304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a5026001".decode('hex')) -SIGNATURE_2 = bytearray("30440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db01".decode('hex')) -# Armory supporttx -TRANSACTION = bytearray("0100000001008338137ef80601faf283aa896bfd80ebb842e290122253cada1afcb876160c00000000fc004730440220634fbbfaaea74d42280a8c9e56c97418af04539f93458e85285d15462aec7712022041ba27a5644642a2f5b3c02610235ec2c6115bf4137bb51181cbc0a3a54dc0db0147304402205545419c4aded39c7f194b3f8c828f90e8d9352c756f7c131ed50e189c02f29a02201b160503d7310df49055b04a327e185fc22dfe68f433594ed7ce526d99a50260014c6952210269694830114e4b1f6ef565ce4efb933681032d30333c80df713df6b60a4c62832102f43b905e9e35ccd22757faedf9eceb652dc9ba198a3904d43f4298def0213eb521037b9e3578dd3b5559d613bc2641931e6ce7d55a9d081b07347888d7d17a2b910253aeffffffff02809698000000000017a914c0c3b6ada732c797881d00de6c350eec96e3d22287f02925020000000017a914e2a227eb40dfce902f2c1d80ddafa798b16d22c38700000000".decode('hex')) - -# Armory txoutscript -output = get_output_script([["0.1", bytearray("a914c0c3b6ada732c797881d00de6c350eec96e3d22287".decode('hex'))], ["0.3599", bytearray("a914e2a227eb40dfce902f2c1d80ddafa798b16d22c387".decode('hex'))]]); -if output<>OUTPUT: - raise BTChipException("Invalid output script encoding"); - -# Optional setup -dongle = getDongle(True) -app = btchip(dongle) -try: - app.setup(btchip.OPERATION_MODE_RELAXED_WALLET, btchip.FEATURE_RFC6979|btchip.FEATURE_NO_2FA_P2SH, 111, 196, "1234", None, btchip.QWERTY_KEYMAP, SEED) -except Exception: - pass -# Authenticate -app.verifyPin("1234") -# Get the trusted input associated to the UTXO -transaction = bitcoinTransaction(UTX) -print transaction -trustedInput = app.getTrustedInput(transaction, UTXO_INDEX) -# Start composing the transaction -app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) -app.finalizeInputFull(OUTPUT) -signature1 = app.untrustedHashSign("0'/0/1", "") -if signature1 <> SIGNATURE_1: - raise BTChipException("Invalid signature1") - -# Same thing for the second signature - -app.startUntrustedTransaction(True, 0, [trustedInput], REDEEMSCRIPT) -app.finalizeInputFull(OUTPUT) -signature2 = app.untrustedHashSign("0'/0/2", "") -if signature2 <> SIGNATURE_2: - raise BTChipException("Invalid signature2") - -# Finalize the transaction - build the redeem script and put everything together -inputScript = get_p2sh_input_script(REDEEMSCRIPT, [signature2, signature1]) -transaction = format_transaction(OUTPUT, [ [ trustedInput['value'], inputScript] ]) -print "Generated transaction : " + str(transaction).encode('hex') -if transaction <> TRANSACTION: - raise BTChipException("Invalid transaction") -# The transaction is ready to be broadcast, enjoy - diff --git a/tests/btchippython/tests/testSimpleTransaction.py b/tests/btchippython/tests/testSimpleTransaction.py deleted file mode 100644 index 3b2abffd..00000000 --- a/tests/btchippython/tests/testSimpleTransaction.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -******************************************************************************* -* BTChip Bitcoin Hardware Wallet Python API -* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************** -""" - -from btchip.btchip import * -from btchip.btchipUtils import * - -# Run on non configured dongle or dongle configured with test seed below - -SEED = bytearray("1762F9A3007DBC825D0DD9958B04880284E88F10C57CF569BB3DADF7B1027F2D".decode('hex')) - -UTX = bytearray("01000000014ea60aeac5252c14291d428915bd7ccd1bfc4af009f4d4dc57ae597ed0420b71010000008a47304402201f36a12c240dbf9e566bc04321050b1984cd6eaf6caee8f02bb0bfec08e3354b022012ee2aeadcbbfd1e92959f57c15c1c6debb757b798451b104665aa3010569b49014104090b15bde569386734abf2a2b99f9ca6a50656627e77de663ca7325702769986cf26cc9dd7fdea0af432c8e2becc867c932e1b9dd742f2a108997c2252e2bdebffffffff0281b72e00000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88aca0860100000000001976a9144533f5fb9b4817f713c48f0bfe96b9f50c476c9b88ac00000000".decode('hex')) -UTXO_INDEX = 1 -ADDRESS = "1BTChipvU14XH6JdRiK9CaenpJ2kJR9RnC" -AMOUNT = "0.0009" -FEES = "0.0001" - -SECONDFACTOR_1 = "Powercycle then confirm transfer of 0.0009 BTC to 1BTChipvU14XH6JdRiK9CaenpJ2kJR9RnC fees 0.0001 BTC change 0 BTC with PIN" -SIGNATURE = bytearray("3045022100ea6df031b47629590daf5598b6f0680ad0132d8953b401577f01e8cc46393fe602202201b7a19d706a0213dcfeb7033719b92c6fd58a2d1d53411de71c4d8353154b01".decode('hex')) -TRANSACTION = bytearray("0100000001c773da236484dae8f0fdba3d7e0ba1d05070d1a34fc44943e638441262a04f10010000006b483045022100ea6df031b47629590daf5598b6f0680ad0132d8953b401577f01e8cc46393fe602202201b7a19d706a0213dcfeb7033719b92c6fd58a2d1d53411de71c4d8353154b01210348bb1fade0adde1bf202726e6db5eacd2063fce7ecf8bbfd17377f09218d5814ffffffff01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac00000000".decode('hex')) - -# Optional setup -dongle = getDongle(True) -app = btchip(dongle) -try: - app.setup(btchip.OPERATION_MODE_WALLET, btchip.FEATURE_RFC6979, 0x00, 0x05, "1234", None, btchip.QWERTY_KEYMAP, SEED) -except Exception: - pass -# Authenticate -app.verifyPin("1234") -# Get the public key and compress it -publicKey = compress_public_key(app.getWalletPublicKey("0'/0/0")['publicKey']) -# Get the trusted input associated to the UTXO -transaction = bitcoinTransaction(UTX) -outputScript = transaction.outputs[UTXO_INDEX].script -trustedInput = app.getTrustedInput(transaction, UTXO_INDEX) -# Start composing the transaction -app.startUntrustedTransaction(True, 0, [trustedInput], outputScript) -outputData = app.finalizeInput(ADDRESS, AMOUNT, FEES, "0'/1/0") -dongle.close() -# Wait for the second factor confirmation -# Done on the same application for test purposes, this is typically done in another window -# or another computer for bigger transactions -response = raw_input("Powercycle the dongle to get the second factor and powercycle again : ") -if not response.startswith(SECONDFACTOR_1): - raise BTChipException("Invalid second factor") -# Get a reference to the dongle again, as it was disconnected -dongle = getDongle(True) -app = btchip(dongle) -# Replay the transaction, this time continue it since the second factor is ready -app.startUntrustedTransaction(False, 0, [trustedInput], outputScript) -app.finalizeInput(ADDRESS, "0.0009", "0.0001", "0'/1/0") -# Provide the second factor to finalize the signature -signature = app.untrustedHashSign("0'/0/0", response[len(response) - 4:]) -if signature <> SIGNATURE: - raise BTChipException("Invalid signature") -# Finalize the transaction - build the redeem script and put everything together -inputScript = get_regular_input_script(signature, publicKey) -transaction = format_transaction(outputData['outputData'], [ [ trustedInput['value'], inputScript] ]) -print "Generated transaction : " + str(transaction).encode('hex') -if transaction <> TRANSACTION: - raise BTChipException("Invalid transaction") -# The transaction is ready to be broadcast, enjoy diff --git a/tests/btchippython/ui/make.sh b/tests/btchippython/ui/make.sh deleted file mode 100755 index 24119f95..00000000 --- a/tests/btchippython/ui/make.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -pyuic4 personalization-00-start.ui -o ../btchip/ui/personalization00start.py -pyuic4 personalization-01-seed.ui -o ../btchip/ui/personalization01seed.py -pyuic4 personalization-02-security.ui -o ../btchip/ui/personalization02security.py -pyuic4 personalization-03-config.ui -o ../btchip/ui/personalization03config.py -pyuic4 personalization-04-finalize.ui -o ../btchip/ui/personalization04finalize.py -pyuic4 personalization-seedbackup-01.ui -o ../btchip/ui/personalizationseedbackup01.py -pyuic4 personalization-seedbackup-02.ui -o ../btchip/ui/personalizationseedbackup02.py -pyuic4 personalization-seedbackup-03.ui -o ../btchip/ui/personalizationseedbackup03.py -pyuic4 personalization-seedbackup-04.ui -o ../btchip/ui/personalizationseedbackup04.py - - diff --git a/tests/btchippython/ui/personalization-00-start.ui b/tests/btchippython/ui/personalization-00-start.ui deleted file mode 100644 index 13cc3e2f..00000000 --- a/tests/btchippython/ui/personalization-00-start.ui +++ /dev/null @@ -1,98 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 231 - - - - BTChip setup - - - - - 120 - 20 - 231 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - - - - - - 20 - 60 - 351 - 61 - - - - Your BTChip dongle is not set up - you'll be able to create a new wallet, or restore an existing one, and choose your security profile. - - - true - - - - - - 310 - 200 - 75 - 25 - - - - Next - - - - - - 20 - 120 - 351 - 81 - - - - Sensitive information including your dongle PIN will be exchanged during this setup phase - it is recommended to execute it on a secure computer, disconnected from any network, especially if you restore a wallet backup. - - - true - - - - - - 20 - 200 - 75 - 25 - - - - Cancel - - - - - - diff --git a/tests/btchippython/ui/personalization-01-seed.ui b/tests/btchippython/ui/personalization-01-seed.ui deleted file mode 100644 index 15ae4ce9..00000000 --- a/tests/btchippython/ui/personalization-01-seed.ui +++ /dev/null @@ -1,160 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 300 - - - - BTChip setup - seed - - - - - 50 - 20 - 311 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - seed (1/3) - - - - - - 20 - 60 - 351 - 61 - - - - Please select an option : either create a new wallet or restore an existing one - - - true - - - - - - 20 - 130 - 94 - 21 - - - - New Wallet - - - true - - - buttonGroup - - - - - - 20 - 180 - 171 - 21 - - - - Restore wallet backup - - - buttonGroup - - - - - false - - - - 50 - 210 - 331 - 21 - - - - QLineEdit::Normal - - - Enter an hexadecimal seed or a BIP 39 mnemonic code - - - - - - 10 - 270 - 75 - 25 - - - - Cancel - - - - - - 320 - 270 - 75 - 25 - - - - Next - - - - - - 130 - 240 - 171 - 31 - - - - - true - - - - Mnemonic API is not available - - - true - - - - - - - - - diff --git a/tests/btchippython/ui/personalization-02-security.ui b/tests/btchippython/ui/personalization-02-security.ui deleted file mode 100644 index e0f0d944..00000000 --- a/tests/btchippython/ui/personalization-02-security.ui +++ /dev/null @@ -1,226 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 503 - - - - BTChip setup - security - - - - - 20 - 20 - 361 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - security (2/3) - - - - - - 10 - 60 - 351 - 61 - - - - Please choose a security profile - - - true - - - - - - 20 - 110 - 81 - 21 - - - - Hardened - - - true - - - buttonGroup - - - - - - 20 - 210 - 81 - 21 - - - - PIN only - - - buttonGroup - - - - - - 50 - 140 - 351 - 61 - - - - You need to remove the dongle and insert it again to get a second factor validation of all operations. Recommended for expert users and to be fully protected against malwares. - - - true - - - - - - 50 - 230 - 351 - 61 - - - - You only need to enter a PIN once when inserting the dongle. Transactions are not protected against malwares - - - true - - - - - - 10 - 470 - 75 - 25 - - - - Cancel - - - - - - 310 - 470 - 75 - 25 - - - - Next - - - - - - 10 - 300 - 351 - 61 - - - - Please choose a PIN associated to the BTChip dongle. The PIN protects the dongle in case it is stolen, and can be up to 32 characters. The dongle is wiped if a wrong PIN is entered 3 times in a row. - - - true - - - - - - 20 - 380 - 161 - 31 - - - - Enter the new PIN : - - - true - - - - - - 210 - 380 - 161 - 21 - - - - QLineEdit::Password - - - - - - 210 - 420 - 161 - 21 - - - - QLineEdit::Password - - - - - - 20 - 420 - 171 - 31 - - - - Repeat the new PIN : - - - true - - - - - - - - - diff --git a/tests/btchippython/ui/personalization-03-config.ui b/tests/btchippython/ui/personalization-03-config.ui deleted file mode 100644 index 8332dd9e..00000000 --- a/tests/btchippython/ui/personalization-03-config.ui +++ /dev/null @@ -1,136 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 243 - - - - BTChip setup - - - - - 30 - 10 - 361 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - config (3/3) - - - - - - 20 - 50 - 351 - 61 - - - - Please select your keyboard type to type the second factor confirmation - - - true - - - - - - 50 - 110 - 94 - 21 - - - - QWERTY - - - true - - - keyboardGroup - - - - - - 50 - 140 - 94 - 21 - - - - QWERTZ - - - keyboardGroup - - - - - - 50 - 170 - 94 - 21 - - - - AZERTY - - - keyboardGroup - - - - - - 10 - 210 - 75 - 25 - - - - Cancel - - - - - - 320 - 210 - 75 - 25 - - - - Next - - - - - - - - - diff --git a/tests/btchippython/ui/personalization-04-finalize.ui b/tests/btchippython/ui/personalization-04-finalize.ui deleted file mode 100644 index f3093b5b..00000000 --- a/tests/btchippython/ui/personalization-04-finalize.ui +++ /dev/null @@ -1,122 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 267 - - - - BTChip setup - security - - - - - 20 - 20 - 361 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - completed - - - - - - 320 - 230 - 75 - 25 - - - - Finish - - - - - - 10 - 70 - 351 - 61 - - - - BTChip setup is completed. Please enter your PIN to validate it then press Finish - - - true - - - - - - 50 - 140 - 121 - 21 - - - - BTChip PIN : - - - true - - - - - - 200 - 140 - 181 - 21 - - - - QLineEdit::Password - - - - - - 120 - 170 - 171 - 31 - - - - - true - - - - Remaining attempts - - - true - - - - - - - - - diff --git a/tests/btchippython/ui/personalization-seedbackup-01.ui b/tests/btchippython/ui/personalization-seedbackup-01.ui deleted file mode 100644 index f986c035..00000000 --- a/tests/btchippython/ui/personalization-seedbackup-01.ui +++ /dev/null @@ -1,138 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 300 - - - - BTChip setup - - - - - 30 - 20 - 351 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - seed backup - - - - - - 320 - 270 - 75 - 25 - - - - Next - - - - - - 10 - 100 - 351 - 31 - - - - A new seed has been generated for your wallet. - - - true - - - - - - 10 - 140 - 351 - 31 - - - - You must backup this seed and keep it out of reach of hackers (typically by keeping it on paper). - - - true - - - - - - 10 - 180 - 351 - 41 - - - - You can use this seed to restore your dongle if you lose it or access your funds with any other compatible wallet. - - - true - - - - - - 90 - 60 - 251 - 31 - - - - - 20 - 75 - true - true - - - - READ CAREFULLY - - - - - - 10 - 220 - 351 - 41 - - - - Press Next to start the backuping process. - - - true - - - - - - diff --git a/tests/btchippython/ui/personalization-seedbackup-02.ui b/tests/btchippython/ui/personalization-seedbackup-02.ui deleted file mode 100644 index 4260ab15..00000000 --- a/tests/btchippython/ui/personalization-seedbackup-02.ui +++ /dev/null @@ -1,69 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 300 - - - - BTChip setup - - - - - 30 - 20 - 351 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - seed backup - - - - - - 20 - 70 - 351 - 31 - - - - Please disconnect the dongle then press Next - - - true - - - - - - 320 - 270 - 75 - 25 - - - - Next - - - - - - diff --git a/tests/btchippython/ui/personalization-seedbackup-03.ui b/tests/btchippython/ui/personalization-seedbackup-03.ui deleted file mode 100644 index edc69bcb..00000000 --- a/tests/btchippython/ui/personalization-seedbackup-03.ui +++ /dev/null @@ -1,165 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 513 - - - - BTChip setup - - - - - 20 - 10 - 351 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - seed backup - - - - - - 20 - 50 - 351 - 61 - - - - If you do not trust this computer, perform the following steps on a trusted one or a different device. Anything supporting keyboard input will work (smartphone, TV box ...) - - - true - - - - - - 20 - 120 - 351 - 31 - - - - Open a text editor, set the focus on the text editor, then insert the dongle - - - true - - - - - - 20 - 160 - 351 - 51 - - - - After a very short time, the dongle will type the seed as hexadecimal (0..9 A..F) characters, starting with "seed" and ending with "X" - - - true - - - - - - 20 - 220 - 351 - 51 - - - - If you perform those steps on Windows, a new device driver will be loaded the first time and the seed will not be typed. This is normal. - - - true - - - - - - 20 - 280 - 351 - 71 - - - - If you perform those steps on Mac, you'll get a popup asking you to select a keyboard type the first time and the seed will not be typed. This is normal, just close the popup. - - - true - - - - - - 20 - 350 - 351 - 51 - - - - If you did not see the seed for any reason, keep the focus on the text editor, unplug and plug the dongle again twice. - - - true - - - - - - 20 - 410 - 351 - 51 - - - - Then press Next once you wrote the seed to a safe medium (i.e. paper) and unplugged the dongle - - - true - - - - - - 310 - 480 - 75 - 25 - - - - Next - - - - - - diff --git a/tests/btchippython/ui/personalization-seedbackup-04.ui b/tests/btchippython/ui/personalization-seedbackup-04.ui deleted file mode 100644 index 9662eb76..00000000 --- a/tests/btchippython/ui/personalization-seedbackup-04.ui +++ /dev/null @@ -1,82 +0,0 @@ - - - Dialog - - - - 0 - 0 - 554 - 190 - - - - BTChip setup - - - - - 30 - 10 - 351 - 31 - - - - - 20 - 75 - true - true - - - - BTChip setup - seed backup - - - - - - 10 - 50 - 351 - 51 - - - - Did you see the seed correctly displayed and did you backup it properly ? - - - true - - - - - - 20 - 140 - 501 - 25 - - - - Yes, the seed is backed up properly and kept in a safe place, move on - - - - - - 20 - 110 - 501 - 25 - - - - No, I didn't see the seed. Wipe the dongle and start over - - - - - - diff --git a/tests/clean_tests.sh b/tests/clean_tests.sh index 1660cc3d..83b9187b 100755 --- a/tests/clean_tests.sh +++ b/tests/clean_tests.sh @@ -1,4 +1,4 @@ #!/bin/bash rm -rf bitcoin-bin -rm -rf ravencoin-bin +rm -rf bitcoin-testnet-bin diff --git a/tests/conftest.py b/tests/conftest.py index ea8f5932..01dc5c17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,10 +33,10 @@ def device(request, hid): speculos_executable = os.environ.get("SPECULOS", "speculos.py") base_args = [ - speculos_executable, "./ravencoin-bin/app.elf", + speculos_executable, "./bitcoin-testnet-bin/app.elf", "-l", "Bitcoin:./bitcoin-bin/app.elf", - "--sdk", "2.0", - #"--display", "headless" + "--sdk", "1.6", + "--display", "headless" ] # Look for the automation_file attribute in the test function, if present diff --git a/tests/data/many-to-many/p2pkh/tx.json b/tests/data/many-to-many/p2pkh/tx.json index d35998a6..ce38b5b4 100644 --- a/tests/data/many-to-many/p2pkh/tx.json +++ b/tests/data/many-to-many/p2pkh/tx.json @@ -4,8 +4,8 @@ "amount": 500000, "fees": 400, "to": "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm", - "sign_paths": ["m/44'/175'/0'/1/0", "m/44'/175'/0'/0/3"], - "change_path": "m/44'/175'/0'/1/2", + "sign_paths": ["m/84'/1'/0'/1/0", "m/84'/1'/0'/0/3"], + "change_path": "m/84'/1'/0'/1/2", "lock_time": 1901785, "utxos": [ { diff --git a/tests/data/one-to-one/p2pkh/tx.json b/tests/data/one-to-one/p2pkh/tx.json index e934c1df..cce076fe 100644 --- a/tests/data/one-to-one/p2pkh/tx.json +++ b/tests/data/one-to-one/p2pkh/tx.json @@ -1,17 +1,19 @@ { - "amount": 10200000000, - "fees": 14906044, - "to": "RFkGFbRtzs6YLRaCTiNpMzaqrirv6noVh9", - "sign_paths": ["m/44'/175'/0'/0/0"], + "txid": "f26b62046101b7cd369eafb3aed5bef343ff3849b98b3cf42dea9cdc78b4c2f4", + "raw": "02000000015122c2cde6823e55754175b92c9c57a0a8e1ac83c38e1787fd3a1ff3348e9513010000006b483045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f4012102ee8608207e21028426f69e76447d7e3d5e077049f5e683c3136c2314762a4718fdffffff0178410f00000000001976a91413d7d58166946c3ec022934066d8c0d111d1bb4188ac1a041d00", + "amount": 999800, + "fees": 200, + "to": "mhKsh7EzJo1gSU1vrpyejS1qsJAuKyaWWg", + "sign_paths": ["m/84'/1'/0'/0/0"], "change_path": null, "lock_time": 1901594, "utxos": [ { - "txid": "0b6e055fde23f5e0b5070cead011ac7d847717aa8a4a91bddadb24e609d336fd", - "raw": "02000000013e0b1f94e33b7bc86699e5906aacbc01ca17c4a632e107c5d1561ada157c44f4010000006b483045022100af3f5129d0d78b0684b13f469ef6426430ba49faed21b86b1d5eada404f8725502202ece7659e98f6a4239cb6d3685b88def6c5e120a67235f405a85af131a5d63280121035e6655bb66d3ec8612d3fc7e6656387e085fa3dc26d3ce3f391baccda011c037feffffff02bc18db60020000001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188aca7d6aae8520000001976a91446ef7841d380f258ae1f95f264d4532f93cb28e588ac18a31b00", - "output_indexes": [0], - "output_amounts": [10214906044] + "txid": "13958e34f31f3afd87178ec383ace1a8a0579c2cb9754175553e82e6cdc22251", + "raw": "02000000000101ec230e53095256052a2428270eec0498944b10f6f1c578f431c23d0098b4ae5a0100000017160014281539820e2de973ae41ba6004b431c921c4d86dfeffffff02727275000000000017a914c8b906af298c70e603a28c3efc2fae19e6ab280f8740420f00000000001976a914cbae5b50cf939e6f531b8a6b7abd788fe14b029788ac02473044022037ecb4248361aafd4f8c11e705f0fa7a5fbdcd595172fcd5643f3b11beff5d400220020c6d326f6c37d63cecadaf4eb335faedf7c44e05f5ef1d2b68140b023bd13d012103dac82fc0acfcfc36348d4a48a46f01cea77f2b9ece3f8c3b4c99d0b0b2f995d284f21c00", + "output_indexes": [1], + "output_amounts": [999800] } ] -} \ No newline at end of file +} diff --git a/tests/electrum_clone/electrumravencoin b/tests/electrum_clone/electrumravencoin deleted file mode 160000 index d856e264..00000000 --- a/tests/electrum_clone/electrumravencoin +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d856e2649b367297afb608e82bb9d98111acc49a diff --git a/tests/electrum_clone/ledger_sign_funcs.py b/tests/electrum_clone/ledger_sign_funcs.py deleted file mode 100644 index 6468ed10..00000000 --- a/tests/electrum_clone/ledger_sign_funcs.py +++ /dev/null @@ -1,79 +0,0 @@ -from btchippython.btchip.bitcoinTransaction import bitcoinTransaction -from btchippython.btchip.btchip import btchip -from electrum_clone.electrumravencoin.electrum.transaction import Transaction -from electrum_clone.electrumravencoin.electrum.util import bfh -from electrum_clone.electrumravencoin.electrum.ravencoin import int_to_hex, var_int - -def sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath): - inputs = [] - chipInputs = [] - redeemScripts = [] - output = None - p2shTransaction = False - segwitTransaction = False - pin = "" - - # Fetch inputs of the transaction to sign - for i, txin in enumerate(tx.inputs()): - redeemScript = Transaction.get_preimage_script(txin) - print("REDEEM SCRIPT: {}".format(redeemScript)) - txin_prev_tx = txin.utxo - txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None - inputs.append([txin_prev_tx_raw, - txin.prevout.out_idx, - redeemScript, - txin.prevout.txid.hex(), - pubkeys[i], - txin.nsequence, - txin.value_sats()]) - - txOutput = var_int(len(tx.outputs())) - for o in tx.outputs(): - txOutput += int_to_hex(0 if o.asset else o.value.value, 8) - script = o.scriptpubkey.hex() - txOutput += var_int(len(script) // 2) - txOutput += script - txOutput = bfh(txOutput) - - for utxo in inputs: - - sequence = int_to_hex(utxo[5], 4) - - txtmp = bitcoinTransaction(bfh(utxo[0])) - trustedInput = btchip.getTrustedInput(cmd, txtmp, utxo[1]) - trustedInput['sequence'] = sequence - - chipInputs.append(trustedInput) - print("REDEEM SCRIPT 2: {}".format(txtmp.outputs[utxo[1]].script)) - redeemScripts.append(txtmp.outputs[utxo[1]].script) - - print("INPUTS: {}".format(inputs)) - - # Sign all inputs - firstTransaction = True - inputIndex = 0 - rawTx = tx.serialize_to_network() - - btchip.enableAlternate2fa(cmd, False) - - while inputIndex < len(inputs): - print('SIGNING: {}'.format(redeemScripts[inputIndex])) - btchip.startUntrustedTransaction(cmd, firstTransaction, inputIndex, - chipInputs, redeemScripts[inputIndex], version=tx.version) - # we don't set meaningful outputAddress, amount and fees - # as we only care about the alternateEncoding==True branch - outputData = btchip.finalizeInput(cmd, b'', 0, 0, changePath, bfh(rawTx)) - outputData['outputData'] = txOutput - if outputData['confirmationNeeded']: - outputData['address'] = output - else: - # Sign input with the provided PIN - inputSignature = btchip.untrustedHashSign(cmd, inputsPaths[inputIndex], pin, - lockTime=tx.locktime) - inputSignature[0] = 0x30 # force for 1.4.9+ - my_pubkey = inputs[inputIndex][4] - tx.add_signature_to_txin(txin_idx=inputIndex, - signing_pubkey=my_pubkey.hex(), - sig=inputSignature.hex()) - inputIndex = inputIndex + 1 - firstTransaction = False \ No newline at end of file diff --git a/tests/prepare_tests.sh b/tests/prepare_tests.sh index 2ad2fc4c..b7623638 100755 --- a/tests/prepare_tests.sh +++ b/tests/prepare_tests.sh @@ -4,5 +4,5 @@ make clean make -j DEBUG=1 # compile optionally with PRINTF mv bin/ tests/bitcoin-bin make clean -make -j DEBUG=1 COIN=ravencoin -mv bin/ tests/ravencoin-bin +make -j DEBUG=1 COIN=bitcoin_testnet +mv bin/ tests/bitcoin-testnet-bin diff --git a/tests/test_get_coin_version.py b/tests/test_get_coin_version.py index d4ef15fd..e475a9a5 100644 --- a/tests/test_get_coin_version.py +++ b/tests/test_get_coin_version.py @@ -1,10 +1,9 @@ def test_get_coin_version(cmd): - #(p2pkh_prefix, p2sh_prefix, coin_family, coin_name, coin_ticker) = cmd.get_coin_version() + (p2pkh_prefix, p2sh_prefix, coin_family, coin_name, coin_ticker) = cmd.get_coin_version() # Bitcoin app: (0x00, 0x05, 0x01, "Bitcoin", "BTC") - #assert (p2pkh_prefix, - # p2sh_prefix, - # coin_family, - # coin_name, - # coin_ticker) == (60, 122, 0x01, "Raven", "RVN") - pass + assert (p2pkh_prefix, + p2sh_prefix, + coin_family, + coin_name, + coin_ticker) == (0x6F, 0xC4, 0x01, "Bitcoin", "TEST") diff --git a/tests/test_get_firmware_version.py b/tests/test_get_firmware_version.py index c9b2a546..1381cdbc 100644 --- a/tests/test_get_firmware_version.py +++ b/tests/test_get_firmware_version.py @@ -1,5 +1,4 @@ def test_get_version(cmd): - #major, minor, patch = cmd.get_firmware_version() # type: int, int, int + major, minor, patch = cmd.get_firmware_version() # type: int, int, int - #assert (1, 4, 8) <= (major, minor, patch) < (1, 6, 2) - pass + assert (1, 4, 8) <= (major, minor, patch) < (1, 6, 0) diff --git a/tests/test_get_pubkey.py b/tests/test_get_pubkey.py new file mode 100644 index 00000000..1dfa6939 --- /dev/null +++ b/tests/test_get_pubkey.py @@ -0,0 +1,42 @@ +from bitcoin_client.bitcoin_base_cmd import AddrType + + +def test_get_public_key(cmd): + # legacy address + pub_key, addr, bip32_chain_code = cmd.get_public_key( + addr_type=AddrType.Legacy, + bip32_path="m/44'/1'/0'/0/0", + display=False + ) + + assert pub_key == bytes.fromhex("04" + "ee8608207e21028426f69e76447d7e3d5e077049f5e683c3136c2314762a4718" + "b45f5224b05ebbad09f43594b7bd8dc0eff4519a07cbab37ecc66e0001ab959a") + assert addr == "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm" + assert bip32_chain_code == bytes.fromhex("0322c8f681e7274e767cee09b8e41770e6d2afd504fd5f85dfaab3e1ff6cdfcc") + + # P2SH-P2WPKH address + pub_key, addr, bip32_chain_code = cmd.get_public_key( + addr_type=AddrType.P2SH_P2WPKH, + bip32_path="m/49'/1'/0'/0/0", + display=False + ) + + assert pub_key == bytes.fromhex("04" + "a80f007194d53d37f6f99539f635390588c4e1c328b098295f61af40d60cb28a" + "7b5649e8121c89148fbab7d693108654685b4e939d9c1bc55a71b43f389929fe") + assert addr == "2MyHkbusvLomaarGYMqyq7q9pSBYJRwWcsw" + assert bip32_chain_code == bytes.fromhex("dc699bc018541f456df1ebd4dea516a633a6260e0a701eba143449adc2ca63f3") + + # bech32 address + pub_key, addr, bip32_chain_code = cmd.get_public_key( + addr_type=AddrType.BECH32, + bip32_path="m/84'/1'/0'/0/0", + display=False + ) + + assert pub_key == bytes.fromhex("04" + "7cb75d34b005c4eb9f62bbf2c457d7638e813e757efcec8fa68677d950b63662" + "648e4f638cabc4e4383fa3fe8348456e46fa56742dcf500a5b50dc1d403492f0") + assert addr == "tb1qzdr7s2sr0dwmkwx033r4nujzk86u0cy6fmzfjk" + assert bip32_chain_code == bytes.fromhex("efd851020a3827ba0d3fd4375910f0ed55dbe8c5d740b37559e993b1d623a956") diff --git a/tests/test_get_random.py b/tests/test_get_random.py new file mode 100644 index 00000000..91bb8f9f --- /dev/null +++ b/tests/test_get_random.py @@ -0,0 +1,15 @@ +import pytest + +from bitcoin_client.exception import IncorrectLengthError + + +def test_random(cmd): + r: bytes = cmd.get_random(n=5) + assert len(r) == 5 + + r = cmd.get_random(n=32) + assert len(r) == 32 + + # max lenght is 248! + with pytest.raises(IncorrectLengthError): + cmd.get_random(n=249) diff --git a/tests/test_get_trusted_inputs.py b/tests/test_get_trusted_inputs.py index fdee713c..7c2ff475 100644 --- a/tests/test_get_trusted_inputs.py +++ b/tests/test_get_trusted_inputs.py @@ -154,21 +154,21 @@ def test_get_trusted_inputs(cmd): tx.calc_sha256() output_index = 0 - #trusted_input = cmd.get_trusted_input(utxo=tx, output_index=output_index) + trusted_input = cmd.get_trusted_input(utxo=tx, output_index=output_index) - #_, _, _, prev_txid, out_index, amount, _ = deser_trusted_input(trusted_input) - #assert out_index == output_index - #assert prev_txid == tx.sha256.to_bytes(32, byteorder="little") - #assert amount == tx.vout[out_index].nValue + _, _, _, prev_txid, out_index, amount, _ = deser_trusted_input(trusted_input) + assert out_index == output_index + assert prev_txid == tx.sha256.to_bytes(32, byteorder="little") + assert amount == tx.vout[out_index].nValue bip141_tx = CTransaction() bip141_tx.deserialize(BytesIO(bip141_raw_tx)) bip141_tx.calc_sha256() output_index = 1 - #trusted_input = cmd.get_trusted_input(utxo=bip141_tx, output_index=output_index) + trusted_input = cmd.get_trusted_input(utxo=bip141_tx, output_index=output_index) - #_, _, _, prev_txid, out_index, amount, _ = deser_trusted_input(trusted_input) - #assert out_index == output_index - #assert prev_txid == bip141_tx.sha256.to_bytes(32, byteorder="little") - #assert amount == bip141_tx.vout[out_index].nValue + _, _, _, prev_txid, out_index, amount, _ = deser_trusted_input(trusted_input) + assert out_index == output_index + assert prev_txid == bip141_tx.sha256.to_bytes(32, byteorder="little") + assert amount == bip141_tx.vout[out_index].nValue diff --git a/tests/test_pubkey.py b/tests/test_pubkey.py deleted file mode 100644 index 8fc62a64..00000000 --- a/tests/test_pubkey.py +++ /dev/null @@ -1,32 +0,0 @@ -from bitcoin_client.bitcoin_base_cmd import AddrType -from bitcoin_client.hwi.base58 import decode as base58_decode - - -def test_get_public_key(cmd): - # legacy address - - paths =[ - "m/44'/175'/0'/0/0", - "m/44'/175'/0'/0/1", - "m/44'/175'/0'/0/2", - "m/44'/175'/0'/0/3", - "m/44'/175'/0'/0/4", - "m/44'/175'/0'/1/0", - "m/44'/175'/0'/1/1", - "m/44'/175'/0'/1/2", - "m/44'/175'/0'/1/3", - "m/44'/175'/0'/1/4", - ] - - addrs = [] - - for path in paths: - pub_key, addr, bip32_chain_code = cmd.get_public_key( - addr_type=AddrType.Legacy, - bip32_path=path, - display=False - ) - addrs.append((pub_key, addr, base58_decode(addr)[1:21].hex())) - - print("ADDRESSES:") - print(addrs) \ No newline at end of file diff --git a/tests/test_sign.py b/tests/test_sign.py new file mode 100644 index 00000000..4fede15c --- /dev/null +++ b/tests/test_sign.py @@ -0,0 +1,89 @@ +from hashlib import sha256 +import json +from pathlib import Path +from typing import Tuple, List, Dict, Any +import pytest + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + +from bitcoin_client.hwi.serialization import CTransaction +from bitcoin_client.exception import ConditionOfUseNotSatisfiedError +from utils import automation + + +def sign_from_json(cmd, filepath: Path): + tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) + + raw_utxos: List[Tuple[bytes, int]] = [ + (bytes.fromhex(utxo_dct["raw"]), output_index) + for utxo_dct in tx_dct["utxos"] + for output_index in utxo_dct["output_indexes"] + ] + to_address: str = tx_dct["to"] + to_amount: int = tx_dct["amount"] + fees: int = tx_dct["fees"] + + sigs = cmd.sign_new_tx(address=to_address, + amount=to_amount, + fees=fees, + change_path=tx_dct["change_path"], + sign_paths=tx_dct["sign_paths"], + raw_utxos=raw_utxos, + lock_time=tx_dct["lock_time"]) + + expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) + witnesses = expected_tx.wit.vtxinwit + for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): + expected_der_sig, expected_pubkey = witness.scriptWitness.stack + assert expected_pubkey == sign_pub_key + assert expected_der_sig == der_sig + pk: VerifyingKey = VerifyingKey.from_string( + sign_pub_key, + curve=SECP256k1, + hashfunc=sha256 + ) + assert pk.verify_digest(signature=der_sig[:-1], # remove sighash + digest=tx_hash_digest, + sigdecode=sigdecode_der) is True + + +def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): + # payloads do not matter, should check and fail before checking it (but non-empty is required) + sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") + assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" + sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") + assert sw == 0x6B00, "should fail with non-zero p1" + sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") + assert sw == 0x6B00, "should fail with non-zero p2" + + +def test_untrusted_hash_sign_fail_short_payload(cmd, transport): + # should fail if the payload is less than 7 bytes + sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") + assert sw == 0x6700 + + +@automation("automations/accept.json") +def test_sign_p2wpkh_accept(cmd): + for filepath in Path("data").rglob("p2wpkh/tx.json"): + sign_from_json(cmd, filepath) + + +@automation("automations/accept.json") +def test_sign_p2sh_p2wpkh_accept(cmd): + for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): + sign_from_json(cmd, filepath) + + +@automation("automations/accept.json") +def test_sign_p2pkh_accept(cmd): + for filepath in Path("data").rglob("p2pkh/tx.json"): + sign_from_json(cmd, filepath) + + +@automation("automations/reject.json") +def test_sign_fail_p2pkh_reject(cmd): + with pytest.raises(ConditionOfUseNotSatisfiedError): + sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_asset.py b/tests/test_sign_asset.py deleted file mode 100644 index 2ead716d..00000000 --- a/tests/test_sign_asset.py +++ /dev/null @@ -1,136 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.bitcoin_utils import compress_pub_key -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - comp = compress_pub_key(b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('0200000002905125de74ccf2772ad166bc7dec6115cd2293f25bebb2c22853a37a1859fb81000000006b483045022100fd3f1d4904a3131f23c427cf35badaa0747aa631e52a172b3fcfadf0268dabe9022034a5f4fd6ca75f9e54279f4b2775e3598db9997c8004a252d8a61e09ca0f091b01210351d088a9964f496a980f5e465c07c3647bf822562237f836cee3390520b261e9feffffff27366e81ca90bdfe13e07a878cadc5b8cfeb17af913dd6b04d6995b91e272262000000006b483045022100ab5c82658cb937e9a2630315e851c12937a3fded88702078f3013642c9796ceb02200c75cfd83839d09afd7accf6d116ca1c65ea0d2aaa366b205a5296868a6f4ea4012103fc2972ec144b6d72e4b8c03b007c2c7f02ac24ee41f5c5f7afe1a089e13ce8e3feffffff0206661500000000001976a91464c3646f741601535a6933367f30684ed7ab002788ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00e1f505000000007556ae1b00') - vin_prevout = transaction.TxOutpoint.from_str('bfefc88012690f1de0c75a972d3f0b6e3f7ad6e6dd9b95770d92f26d72c9ba8e:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00e1f5050000000075')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [comp] - - in_tx2 = transaction.Transaction('0200000002f9b3284f2483378bbbe6547c990e235eb28c116c1668104d658497fc6ce7ec61000000006a473044022041a7a065faa2ba5a22723c5393d915abe6ab7ef51e6ec9a810c925cfa4dffbbd022027a0f9ca5220cf6ddc5295cbeb76b82254955099c63a4fe6ad4945356b501481012103486e130d517cc6f2b8e4fa435f7885ed1514ac0797cc37f906e58abd49ae3592feffffffc6663d94a04fa54d1d066abdcd5170ca4a180de44eb34e0942f33eb5a5ea73cd000000006a4730440220653a82aa1a46d4d3b0f101aab6bffdb1cc5ccff2d10e3ff08e701493a8677e640220019582b798b2611951d1a8c297eddde1e9aa5719706aa623cff9c7e86b0b2bb70121036e41ad9136a9dd1c2942fa63ad336c52a3c90839d3fd076e364aaf97c4748e57feffffff0269d88d00000000001976a9146d3f15868643bcf95224f0cc865b680386cfb8ef88ac00e1f505000000001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac59ae1b00') - vin_prevout = transaction.TxOutpoint.from_str('8dec0a2c4921ede211df3097f3ceed59d0ad099d353e728f5a39ca309d220440:1') - vin2 = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) - vin2.utxo = in_tx2 - vin2.script_type = 'p2pkh' - vin2.pubkeys = [comp] - - inputs = [ - vin, - vin2 - ] - - vout = transaction.PartialTxOutput(asset='SCAMCOIN', value=1, scriptpubkey=bytes.fromhex('76a9146d3f15868643bcf95224f0cc865b680386cfb8ef88acc01572766e74085343414d434f494e00e1f5050000000075')) - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [comp, comp] - inputsPaths = ["44'/175'/0'/0/0", - "44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - - print(tx.is_complete()) - print(tx.serialize()) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_asset_1_1.py b/tests/test_sign_asset_1_1.py deleted file mode 100644 index 17f54930..00000000 --- a/tests/test_sign_asset_1_1.py +++ /dev/null @@ -1,124 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') - vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - - inputs = [ - vin - ] - - vout = transaction.PartialTxOutput(value=0, scriptpubkey=bytes.fromhex('76a9149730c97deaedf77d8d9c707a506bca4babadaf6988acc01572766e74085343414d434f494e00a3e1110000000075')) - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - inputsPaths = ["44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - - print(tx.is_complete()) - print(tx.serialize()) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_asset_to_large_1_1.py b/tests/test_sign_asset_to_large_1_1.py deleted file mode 100644 index 77cb2a59..00000000 --- a/tests/test_sign_asset_to_large_1_1.py +++ /dev/null @@ -1,121 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') - vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - - inputs = [ - vin - ] - - vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvnt\x21SCAMCOINSCAMCOINSCAMCOINSCAMCOIN1\x00\xa3\xe1\x11\x00\x00\x00\x00u') - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - inputsPaths = ["44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_asset_to_small_1_1.py b/tests/test_sign_asset_to_small_1_1.py deleted file mode 100644 index 3fd6055e..00000000 --- a/tests/test_sign_asset_to_small_1_1.py +++ /dev/null @@ -1,121 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') - vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - - inputs = [ - vin - ] - - vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvnt\x08SCAMCOIN\x00\xa3\xe1\x11\x00\x00\x00u') - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - inputsPaths = ["44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_asset_with_rvn_amount.py b/tests/test_sign_asset_with_rvn_amount.py deleted file mode 100644 index 71149dc6..00000000 --- a/tests/test_sign_asset_with_rvn_amount.py +++ /dev/null @@ -1,121 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') - vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - - inputs = [ - vin - ] - - vout = transaction.PartialTxOutput(value=1, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvnt\x08SCAMCOIN\x00\xa3\xe1\x11\x00\x00\x00\x00u') - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - inputsPaths = ["44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_message.py b/tests/test_sign_message.py deleted file mode 100644 index 8477ca56..00000000 --- a/tests/test_sign_message.py +++ /dev/null @@ -1,109 +0,0 @@ -import re -import base64 -import hashlib -from bitcoin_client.bitcoin_base_cmd import AddrType - -def test_sign_message(cmd): - message_og = 'Test' - path_og = "m/44'/175'/0'/0/0" - - # From https://github.com/LedgerHQ/btchip-python/blob/master/btchip/btchip.py - - # Prepare signing - - def writeUint32BE(value, buffer): - buffer.append((value >> 24) & 0xff) - buffer.append((value >> 16) & 0xff) - buffer.append((value >> 8) & 0xff) - buffer.append(value & 0xff) - return buffer - - def parse_bip32_path(path): - if len(path) == 0: - return bytearray([ 0 ]) - result = [] - elements = path.split('/') - if len(elements) > 10: - raise Exception("Path too long") - for pathElement in elements: - element = re.split('\'|h|H', pathElement) - if len(element) == 1: - writeUint32BE(int(element[0]), result) - else: - writeUint32BE(0x80000000 | int(element[0]), result) - return bytearray([ len(elements) ] + result) - - path = parse_bip32_path(path_og[2:]) - message = message_og.encode('utf8') - - result = {} - offset = 0 - encryptedOutputData = b"" - while (offset < len(message)): - params = []; - if offset == 0: - params.extend(path) - params.append((len(message) >> 8) & 0xff) - params.append(len(message) & 0xff) - p2 = 0x01 - else: - p2 = 0x80 - blockLength = 255 - len(params) - if ((offset + blockLength) < len(message)): - dataLength = blockLength - else: - dataLength = len(message) - offset - params.extend(bytearray(message[offset : offset + dataLength])) - apdu = [ 0xe0, 0x4e, 0x00, p2 ] - apdu.append(len(params)) - apdu.extend(params) - _, response = cmd.transport.exchange_raw(bytearray(apdu)) - encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] - offset += blockLength - result['confirmationNeeded'] = response[1 + response[0]] != 0x00 - result['confirmationType'] = response[1 + response[0]] - if result['confirmationType'] == 0x03: - offset = 1 + response[0] + 1 - result['secureScreenData'] = response[offset:] - result['encryptedOutputData'] = encryptedOutputData - - # Sign - - print('Message Hash') - print(hashlib.sha256(message).hexdigest().upper()) - - apdu = [ 0xe0, 0x4e, 0x80, 0x00 ] - params = [] - params.append(0x00) - apdu.append(len(params)) - apdu.extend(params) - _, signature = cmd.transport.exchange_raw(bytearray(apdu)) - - # Parse the ASN.1 signature - rLength = signature[3] - r = signature[4: 4 + rLength] - sLength = signature[4 + rLength + 1] - s = signature[4 + rLength + 2:] - if rLength == 33: - r = r[1:] - if sLength == 33: - s = s[1:] - # And convert it - - # Pad r and s points with 0x00 bytes when the point is small to get valid signature. - r_padded = bytes([0x00]) * (32 - len(r)) + r - s_padded = bytes([0x00]) * (32 - len(s)) + s - - p = bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded - pub_key, addr, bip32_chain_code = cmd.get_public_key( - addr_type=AddrType.Legacy, - bip32_path=path_og, - display=False - ) - readable_sig = base64.b64encode(p).decode('ascii') - print(addr) - print(message_og) - print(readable_sig) - - assert 'INyo6gzuMMY9wNvT71+amLPG+zBnL4PO8leCdYvSZGuLaVpvHrFcDFf3Q9Gt0ReRuwIxUSaKa+SGFJoxc8b32Zo='==\ - readable_sig diff --git a/tests/test_sign_owner_1_1.py b/tests/test_sign_owner_1_1.py deleted file mode 100644 index 864208c3..00000000 --- a/tests/test_sign_owner_1_1.py +++ /dev/null @@ -1,124 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') - vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - - inputs = [ - vin - ] - - vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvno\x09SCAMCOIN!u') - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - inputsPaths = ["44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - - print(tx.is_complete()) - print(tx.serialize()) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_owner_to_large_1_1.py b/tests/test_sign_owner_to_large_1_1.py deleted file mode 100644 index c5019320..00000000 --- a/tests/test_sign_owner_to_large_1_1.py +++ /dev/null @@ -1,121 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('0100000004f920881040f729ea2b8babd490598fe7cf5505dd9c126b16f26f8c78c63ca4ff0d0000006b483045022100a29f19cf246734c92c8db7560a23e75439b5a10c29aee685b120c63e736e5aae02206f176de9d51352358fd1107bc177c87140ef7c81cb826e31832482ee2ed99ab40121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff6f4aef2eee7799a3bde398444241d3ca9da0490853a72a23743af821b9ddef4e010000006b483045022100dd12647f9a01a008a20e90c16e983c31d4faeea5e92d7f16ff2f6e29e37acfd20220151b69e509a9bc6343a251718b44391072a2dce628194d02c3835f71682aff500121029ef0491fbfb8926c9458e26bb1c6a5065b4b6556f378245c2e6adb354d999711ffffffff9cb900264d07430bf287e2b7b31e4c305d70f420dff7c19e14d3685da9e35129010000006a473044022073738f4a1a0f099a58785149f1fb82091dbb35d3cc4e91d21adc57ff142d6d4e02206d054c2eb52b75f8a651c02bbcb508712e80de120a9eac4bca598eed74bf47b90121025643f032b617d5f052f6d8cd9f2d40934d117cda0b62a6adeb4267d0d892910effffffff8651d6b8c2196da71adbca724c974a478dca12959390baa0bc03827d39c0f286000000006b48304502210089610c65c1c462d947c663968500023b58c09dbc3a0dfec23a4660cbb5a5f1340220210c0b45183f8c1b939c22574247311f1289feafae53749cc1a144b00e9151dd0121038b7583b785bb79322b6b48428a031d1f0eaf96e3ecb29e2caf506d54b049ce9affffffff02ba224d3c000000001976a9149730c97deaedf77d8d9c707a506bca4babadaf6988ac00000000000000003176a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e111000000007500000000') - vin_prevout = transaction.TxOutpoint.from_str('2bdf24964a886019673d9bae2e579f610d56da7bdbe50de98a8583fd19f65e67:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acc01572766e74085343414d434f494e00a3e1110000000075')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - - inputs = [ - vin - ] - - vout = transaction.PartialTxOutput(value=0, scriptpubkey=b'v\xa9\x14\x970\xc9}\xea\xed\xf7}\x8d\x9cpzPk\xcaK\xab\xad\xafi\x88\xac\xc0\x15rvno\x21SCAMCOINSCAMCOINSCAMCOINSCAMCOIN!u') - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - inputsPaths = ["44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_sign_rvn_1_1.py b/tests/test_sign_rvn_1_1.py deleted file mode 100644 index 9844d981..00000000 --- a/tests/test_sign_rvn_1_1.py +++ /dev/null @@ -1,124 +0,0 @@ -from hashlib import sha256 -import json -from pathlib import Path -from typing import Tuple, List, Dict, Any -import pytest - -from ecdsa.curves import SECP256k1 -from ecdsa.keys import VerifyingKey -from ecdsa.util import sigdecode_der - -from bitcoin_client.hwi.serialization import CTransaction -from bitcoin_client.exception import ConditionOfUseNotSatisfiedError -from utils import automation -from electrum_clone.ledger_sign_funcs import sign_transaction -from electrum_clone.electrumravencoin.electrum import transaction - -def sign_from_json(cmd, filepath: Path): - tx_dct: Dict[str, Any] = json.load(open(filepath, "r")) - - raw_utxos: List[Tuple[bytes, int]] = [ - (bytes.fromhex(utxo_dct["raw"]), output_index) - for utxo_dct in tx_dct["utxos"] - for output_index in utxo_dct["output_indexes"] - ] - to_address: str = tx_dct["to"] - to_amount: int = tx_dct["amount"] - fees: int = tx_dct["fees"] - - sigs = cmd.sign_new_tx(address=to_address, - amount=to_amount, - fees=fees, - change_path=tx_dct["change_path"], - sign_paths=tx_dct["sign_paths"], - raw_utxos=raw_utxos, - lock_time=tx_dct["lock_time"]) - - expected_tx = CTransaction.from_bytes(bytes.fromhex(tx_dct["raw"])) - witnesses = expected_tx.wit.vtxinwit - for witness, (tx_hash_digest, sign_pub_key, (v, der_sig)) in zip(witnesses, sigs): - expected_der_sig, expected_pubkey = witness.scriptWitness.stack - assert expected_pubkey == sign_pub_key - assert expected_der_sig == der_sig - pk: VerifyingKey = VerifyingKey.from_string( - sign_pub_key, - curve=SECP256k1, - hashfunc=sha256 - ) - assert pk.verify_digest(signature=der_sig[:-1], # remove sighash - digest=tx_hash_digest, - sigdecode=sigdecode_der) is True - - -#def test_untrusted_hash_sign_fail_nonzero_p1_p2(cmd, transport): -# # payloads do not matter, should check and fail before checking it (but non-empty is required) -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with p1 and p2 both non-zero" -# sw, _ = transport.exchange(0xE0, 0x48, 0x01, 0x00, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p1" -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x01, None, b"\x00") -# assert sw == 0x6B00, "should fail with non-zero p2" - - -#def test_untrusted_hash_sign_fail_short_payload(cmd, transport): -# # should fail if the payload is less than 7 bytes -# sw, _ = transport.exchange(0xE0, 0x48, 0x00, 0x00, None, b"\x01\x02\x03\x04\x05\x06") -# assert sw == 0x6700 - - -#@automation("automations/accept.json") -#def test_sign_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -#def test_sign_p2sh_p2wpkh_accept(cmd): -# for filepath in Path("data").rglob("p2sh-p2wpkh/tx.json"): -# sign_from_json(cmd, filepath) - - -#@automation("automations/accept.json") -def test_sign_p2pkh_accept(cmd): - #for filepath in Path("data").rglob("p2pkh/tx.json"): - # sign_from_json(cmd, filepath) - #sign_from_json(cmd, './data/one-to-one/p2pkh/tx.json') - - tx = transaction.PartialTransaction() - - in_tx = transaction.Transaction('020000000115eb8abda69e314c60a693f1499871fe319587df46b24ab9c89b83e1abb6d7bf010000006b483045022100b776d19d402b062cae744374404cf29f586e94683b5c91183abdde4a2587315202205d68a66ffcb0f96d9aba0d2f3787b07b5f200f2dd87fc345598fa096f83128c8012103c2c6118e389d65e1b281bec87efc71aabb1fa485b63e7639037e442ec61ff5fbfeffffff02794beb56531000001976a914d9f6b08d5ec82b61360988d9619e6656d7b9b75c88ac4b5bbb91210200001976a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188acd2ad1b00') - vin_prevout = transaction.TxOutpoint.from_str('69775d5e61078b15405dfd581713fa2dd4e92231b159e52d80246aead7708693:1') - vin = transaction.PartialTxInput(prevout=vin_prevout, script_sig=bytes.fromhex('76a914edb982a5fbd46f6e12e6a6402e0dfcd791acadd188ac')) - vin.utxo = in_tx - vin.script_type = 'p2pkh' - vin.pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - - inputs = [ - vin - ] - - vout = transaction.PartialTxOutput(value=10, scriptpubkey=bytes.fromhex('76a914c57f73045531ac70dc2c09a1da90fff59df5635588ac')) - - outputs = [ - vout - ] - - tx._inputs = inputs - tx._outputs = outputs - - changePath = '' - - #RWxATTuFXo82CwgixG7Q6npT7JBvDM3Jw9 - #edb982a5fbd46f6e12e6a6402e0dfcd791acadd1 - pubkeys = [b'\x04\x1c\x8a\xab\x06r\xf0\x1d\x10K\x9a9\xc8&\xb2dF\xc8[\xdc\xd1b=\xbf\xb0n\xca\xd6Q\x93W<\xe2\xe0\xec\x18z\xb3X\xc5\xfe\xc0Q\xad\xbe\xeera\xd0\xb4\xc4Y\xe6\xc8\x8b\x7f\\\xcd\xf1x\xbaDS\xe1\x1b'] - inputsPaths = ["44'/175'/0'/0/0"] - - sign_transaction(cmd, tx, pubkeys, inputsPaths, changePath) - - print(tx.is_complete()) - print(tx.serialize()) - -#@automation("automations/reject.json") -#def test_sign_fail_p2pkh_reject(cmd): -# with pytest.raises(ConditionOfUseNotSatisfiedError): -# sign_from_json(cmd, "./data/one-to-one/p2pkh/tx.json") \ No newline at end of file diff --git a/tests/test_verify.py b/tests/test_verify.py deleted file mode 100644 index 1ec26a51..00000000 --- a/tests/test_verify.py +++ /dev/null @@ -1,22 +0,0 @@ -from bitcoin_client.hwi.serialization import CTransaction - -def test_verify(cmd): - firstTransaction = True - inputIndex = 0 - chipInputs = [ - {'trustedInput': True, - 'value': bytearray(b'2\x00]\x97\xf9\xb3(O$\x837\x8b\xbb\xe6T|\x99\x0e#^\xb2\x8c\x11l\x16h\x10Me\x84\x97\xfcl\xe7\xeca\x00\x00\x00\x00\xe0\x0f\x97\x00\x00\x00\x00\x00\xf0\xe0\r\x89\xf2\x84\x1d\x06'), - 'sequence': 'feffffff' - }, - {'trustedInput': True, - 'value': bytearray(b'2\x00\xedSc\x1f\xaay%T\xb0\x92\xfb\xd0\xa1\xc2\xdeC\x81\x01\xe9\n\xad\xd5Vo\x06\xb4e4dZ\xb4\x8b-\xb1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x9c1\x9cJ\xc1\xe9\x0b'), - 'sequence': 'feffffff'} - ] - redeemScripts = [ - b'v\xa9\x14\xb4\x8d\xcc\xf9\xce\x8b\x8a\xfaT\xb9T\x1d Date: Fri, 25 Jun 2021 22:23:15 -0400 Subject: [PATCH 19/20] increase output size --- include/btchip_context.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/btchip_context.h b/include/btchip_context.h index a80f44bb..cd214c4f 100644 --- a/include/btchip_context.h +++ b/include/btchip_context.h @@ -24,7 +24,7 @@ #include "btchip_secure_value.h" #include "btchip_filesystem_tx.h" -#define MAX_OUTPUT_TO_CHECK 100 +#define MAX_OUTPUT_TO_CHECK 120 //Ravencoin asset max #define MAX_COIN_ID 13 #define MAX_SHORT_COIN_ID 5 From 89f381bc372c0ef4a8eac924dbaee160e5420936 Mon Sep 17 00:00:00 2001 From: kralverde Date: Mon, 28 Jun 2021 05:29:32 -0400 Subject: [PATCH 20/20] code optimizations --- src/btchip_apdu_hash_input_finalize_full.c | 26 ++++++++++------------ src/btchip_helpers.c | 15 +++++-------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/btchip_apdu_hash_input_finalize_full.c b/src/btchip_apdu_hash_input_finalize_full.c index b5a957b0..ff2c15fc 100644 --- a/src/btchip_apdu_hash_input_finalize_full.c +++ b/src/btchip_apdu_hash_input_finalize_full.c @@ -47,7 +47,6 @@ static bool check_output_displayable() { unsigned char amount[8], isOpReturn, isP2sh, isNativeSegwit, j, nullAmount = 1; unsigned char isOpCreate, isOpCall; - unsigned char isRavencoinAsset; for (j = 0; j < 8; j++) { if (btchip_context_D.currentOutput[j] != 0) { @@ -80,15 +79,6 @@ static bool check_output_displayable() { isOpCall = btchip_output_script_is_op_call(btchip_context_D.currentOutput + 8, sizeof(btchip_context_D.currentOutput) - 8); - isRavencoinAsset = - nullAmount && - ((-1 != btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8)) - || - (btchip_output_script_get_ravencoin_asset_ptr( - btchip_context_D.currentOutput + 8, - sizeof(btchip_context_D.currentOutput) - 8, - &dummy - ))); if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { PRINTF("Asset script value: %d\n", btchip_output_script_get_ravencoin_asset_ptr( @@ -98,7 +88,6 @@ static bool check_output_displayable() { )); PRINTF("Null asset script value: %d\n", btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8)); PRINTF("Null amount: %d\n", nullAmount); - PRINTF("isAsset: %d\n", isRavencoinAsset); } if (G_coin_config->kind == COIN_KIND_QTUM) { @@ -109,9 +98,18 @@ static bool check_output_displayable() { else if (nullAmount && G_coin_config->kind == COIN_KIND_RAVENCOIN) { // Ravencoin assets only come into play when there is a null amount invalid_script = - !isRavencoinAsset || ( - !btchip_output_script_is_regular_ravencoin_asset(btchip_context_D.currentOutput + 8) && - !isP2sh && !isOpReturn); + (!btchip_output_script_get_ravencoin_asset_ptr( + btchip_context_D.currentOutput + 8, + sizeof(btchip_context_D.currentOutput) - 8, + &dummy) + || + (!btchip_output_script_is_regular_ravencoin_asset(btchip_context_D.currentOutput + 8) + && + !isP2sh + ) + ) + && !isOpReturn + && -1 == btchip_output_script_try_get_ravencoin_asset_tag_type(btchip_context_D.currentOutput + 8); } else { invalid_script = diff --git a/src/btchip_helpers.c b/src/btchip_helpers.c index c7c51abe..8cd8b351 100644 --- a/src/btchip_helpers.c +++ b/src/btchip_helpers.c @@ -18,9 +18,6 @@ #include "btchip_internal.h" #include "btchip_apdu_constants.h" -const unsigned char TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE[] = {0x76, 0xA9, 0x14}; // w/o length -const unsigned char TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE[] = {0xA9, 0x14}; // w/o length - const unsigned char TRANSACTION_OUTPUT_SCRIPT_PRE[] = { 0x19, 0x76, 0xA9, 0x14}; // script length, OP_DUP, OP_HASH160, address length @@ -94,9 +91,9 @@ unsigned char btchip_output_script_is_regular(unsigned char *buffer) { unsigned char btchip_output_script_is_regular_ravencoin_asset(unsigned char *buffer) { if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { - if ((os_memcmp(buffer + 1, TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE, - sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE)) == 0) && - (os_memcmp(buffer + 1 + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_PRE_POST_ONE) + 20, + if ((os_memcmp(buffer + 1, TRANSACTION_OUTPUT_SCRIPT_PRE + 1, + sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE) - 1) == 0) && + (os_memcmp(buffer + sizeof(TRANSACTION_OUTPUT_SCRIPT_PRE) + 20, TRANSACTION_OUTPUT_SCRIPT_POST, sizeof(TRANSACTION_OUTPUT_SCRIPT_POST)) == 0)) { return 1; @@ -128,9 +125,9 @@ unsigned char btchip_output_script_is_p2sh(unsigned char *buffer) { unsigned char btchip_output_script_is_p2sh_ravencoin_asset(unsigned char *buffer) { if (G_coin_config->kind == COIN_KIND_RAVENCOIN) { - if ((os_memcmp(buffer + 1, TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE, - sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE)) == 0) && - (os_memcmp(buffer + 1 + sizeof(TRANSACTION_RAVENCOIN_OUTPUT_SCRIPT_P2SH_PRE_POST_ONE) + 20, + if ((os_memcmp(buffer + 1, TRANSACTION_OUTPUT_SCRIPT_P2SH_PRE + 1, + sizeof(TRANSACTION_OUTPUT_SCRIPT_P2SH_PRE) - 1) == 0) && + (os_memcmp(buffer + sizeof(TRANSACTION_OUTPUT_SCRIPT_P2SH_PRE) + 20, TRANSACTION_OUTPUT_SCRIPT_P2SH_POST, sizeof(TRANSACTION_OUTPUT_SCRIPT_P2SH_POST)) == 0)) { return 1;