From feb4faa67e68e4e33d77077567aed91b8e958942 Mon Sep 17 00:00:00 2001 From: Carlos Medeiros Date: Tue, 23 Jan 2024 15:29:45 +0000 Subject: [PATCH 1/4] fuzzer improvements --- CMakeLists.txt | 12 ++++++------ app/src/eth_erc20.c | 2 +- app/src/parser_impl_common.c | 3 +++ app/src/parser_impl_eth.c | 4 +++- fuzz/parser_parse.cpp | 30 +++++++++++------------------- tests/expected_output.cpp | 7 ++++--- tests/ui_tests.cpp | 10 +++++----- 7 files changed, 33 insertions(+), 35 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cbeb81..1f3ca01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,18 +41,18 @@ if(ENABLE_FUZZING) message(FATAL_ERROR "Fuzz logging enabled") endif() - set(CMAKE_CXX_CLANG_TIDY clang-tidy -checks=-*,bugprone-*,cert-*,clang-analyzer-*,-cert-err58-cpp,misc-*) + set(CMAKE_CXX_CLANG_TIDY clang-tidy -checks=-*,bugprone-*,cert-*,clang-analyzer-*,-cert-err58-cpp,misc-*,-bugprone-suspicious-include) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") # require at least clang 3.2 - if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 10.0) - message(FATAL_ERROR "Clang version must be at least 10.0!") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0) + message(FATAL_ERROR "Clang version must be at least 11.0!") endif() else() message(FATAL_ERROR - "You are using an unsupported compiler! Fuzzing only works with Clang 10.\n" - "1. Install clang-10 \n" - "2. Pass -DCMAKE_C_COMPILER=clang-10 -DCMAKE_CXX_COMPILER=clang++-10") + "You are using an unsupported compiler! Fuzzing only works with Clang 11.\n" + "1. Install clang-11 \n" + "2. Pass -DCMAKE_C_COMPILER=clang-10 -DCMAKE_CXX_COMPILER=clang++-11") endif() string(APPEND CMAKE_C_FLAGS " -fsanitize=fuzzer-no-link") diff --git a/app/src/eth_erc20.c b/app/src/eth_erc20.c index 8509364..a1f1bb8 100644 --- a/app/src/eth_erc20.c +++ b/app/src/eth_erc20.c @@ -97,7 +97,7 @@ parser_error_t printERC20Value(const rlp_t *data, char *outVal, uint16_t outValL bool validateERC20(rlp_t data) { // Check that data start with ERC20 prefix - if (data.rlpLen != ERC20_DATA_LENGTH || memcmp(data.ptr, ERC20_TRANSFER_PREFIX, 4) != 0) { + if (data.ptr == NULL || data.rlpLen != ERC20_DATA_LENGTH || memcmp(data.ptr, ERC20_TRANSFER_PREFIX, 4) != 0) { return false; } diff --git a/app/src/parser_impl_common.c b/app/src/parser_impl_common.c index fae5607..c289ee6 100644 --- a/app/src/parser_impl_common.c +++ b/app/src/parser_impl_common.c @@ -197,6 +197,7 @@ bool parser_output_contains_change_address(parser_context_t *c) { bool contains = false; // verify address is renderable compare with CHANGE ADDRESS #if defined(TARGET_NANOS) || defined(TARGET_NANOS2) || defined(TARGET_NANOX) || defined(TARGET_STAX) + CTX_CHECK_AVAIL(c, ADDRESS_LEN) if (MEMCMP(c->buffer + c->offset, change_address, ADDRESS_LEN) == 0) { contains = false; } else { @@ -205,6 +206,8 @@ bool parser_output_contains_change_address(parser_context_t *c) { #else uint8_t test_change_address[ADDRESS_LEN] = {0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}; + + CTX_CHECK_AVAIL(c, ADDRESS_LEN) if (MEMCMP(c->buffer + c->offset, test_change_address, ADDRESS_LEN) == 0) { contains = false; } else { diff --git a/app/src/parser_impl_eth.c b/app/src/parser_impl_eth.c index b2683ff..45b80a8 100644 --- a/app/src/parser_impl_eth.c +++ b/app/src/parser_impl_eth.c @@ -41,9 +41,11 @@ static parser_error_t readChainID(parser_context_t *ctx, rlp_t *chainId) { uint64_t tmpChainId = 0; if (chainId->rlpLen > 0) { CHECK_ERROR(be_bytes_to_u64(chainId->ptr, chainId->rlpLen, &tmpChainId)) - } else { + } else if (chainId->kind == RLP_KIND_BYTE) { // case were the prefix is the byte itself tmpChainId = chainId->ptr[0]; + } else { + return parser_unexpected_error; } // Check allowed values for chain id diff --git a/fuzz/parser_parse.cpp b/fuzz/parser_parse.cpp index 220f3a3..096b33c 100644 --- a/fuzz/parser_parse.cpp +++ b/fuzz/parser_parse.cpp @@ -9,21 +9,20 @@ #error "This fuzz target won't work correctly with NDEBUG defined, which will cause asserts to be eliminated" #endif - using std::size_t; namespace { - char PARSER_KEY[16384]; - char PARSER_VALUE[16384]; -} +char PARSER_KEY[16384]; +char PARSER_VALUE[16384]; +} // namespace -extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { parser_tx_t txObj; MEMZERO(&txObj, sizeof(txObj)); parser_context_t ctx; parser_error_t rc; + ctx.tx_type = flr_tx; rc = parser_parse(&ctx, data, size, &txObj); if (rc != parser_ok) { return 0; @@ -37,9 +36,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) uint8_t num_items; rc = parser_getNumItems(&ctx, &num_items); if (rc != parser_ok) { - fprintf(stderr, - "error in parser_getNumItems: %s\n", - parser_getErrorDescription(rc)); + (void)fprintf(stderr, "error in parser_getNumItems: %s\n", parser_getErrorDescription(rc)); assert(false); } @@ -49,19 +46,14 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) uint8_t page_idx = 0; uint8_t page_count = 1; while (page_idx < page_count) { - rc = parser_getItem(&ctx, i, - PARSER_KEY, sizeof(PARSER_KEY), - PARSER_VALUE, sizeof(PARSER_VALUE), - page_idx, &page_count); + rc = parser_getItem(&ctx, i, PARSER_KEY, sizeof(PARSER_KEY), PARSER_VALUE, sizeof(PARSER_VALUE), page_idx, + &page_count); -// (void)fprintf(stderr, "%s = %s\n", PARSER_KEY, PARSER_VALUE); + // (void)fprintf(stderr, "%s = %s\n", PARSER_KEY, PARSER_VALUE); if (rc != parser_ok) { - (void)fprintf(stderr, - "error getting item %u at page index %u: %s\n", - (unsigned)i, - (unsigned)page_idx, - parser_getErrorDescription(rc)); + (void)fprintf(stderr, "error getting item %u at page index %u: %s\n", (unsigned)i, (unsigned)page_idx, + parser_getErrorDescription(rc)); assert(false); } diff --git a/tests/expected_output.cpp b/tests/expected_output.cpp index 90bc83f..b1226f4 100644 --- a/tests/expected_output.cpp +++ b/tests/expected_output.cpp @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. ********************************************************************************/ + #include #include @@ -28,7 +29,7 @@ void addTo(std::vector &answer, const S &format_str, Args &&...args answer.push_back(fmt::format(format_str, args...)); } -std::vector FormatEthAddress(const uint32_t idx, const std::string &name, const std::string &address) { +std::vector FormatEthAddress(const std::string &name, const uint32_t idx, const std::string &address) { auto answer = std::vector(); uint8_t numPages = 0; char outBuffer[100]; @@ -72,12 +73,12 @@ std::vector EVMGenerateExpectedUIOutput(const Json::Value &json, bo /// uint8_t idx = 0; - auto destAddress = FormatEthAddress(idx, "To", to); + auto destAddress = FormatEthAddress("To", idx, to); answer.insert(answer.end(), destAddress.begin(), destAddress.end()); if (value.compare(0, 2, "??") == 0) { idx++; - auto contractAddress = FormatEthAddress(idx, "Contract", contract); + auto contractAddress = FormatEthAddress("Contract", idx, contract); answer.insert(answer.end(), contractAddress.begin(), contractAddress.end()); } diff --git a/tests/ui_tests.cpp b/tests/ui_tests.cpp index 47c9a0b..1bfaf6c 100644 --- a/tests/ui_tests.cpp +++ b/tests/ui_tests.cpp @@ -47,10 +47,10 @@ class JsonTestsA : public ::testing::TestWithParam { std::vector GetJsonTestCases(std::string jsonFile) { auto answer = std::vector(); - Json::CharReaderBuilder builder; + const Json::CharReaderBuilder builder; Json::Value obj; - std::string fullPathJsonFile = std::string(TESTVECTORS_DIR) + jsonFile; + const std::string fullPathJsonFile = std::string(TESTVECTORS_DIR) + jsonFile; std::ifstream inFile(fullPathJsonFile); if (!inFile.is_open()) { @@ -94,10 +94,10 @@ template std::vector GetEVMJsonTestCases(const std::string &jsonFile, Generator gen_ui_output) { auto answer = std::vector(); - Json::CharReaderBuilder builder; + const Json::CharReaderBuilder builder; Json::Value obj; - std::string fullPathJsonFile = std::string(TESTVECTORS_DIR) + jsonFile; + const std::string fullPathJsonFile = std::string(TESTVECTORS_DIR) + jsonFile; std::ifstream inFile(fullPathJsonFile); if (!inFile.is_open()) { @@ -129,7 +129,7 @@ void check_testcase(const testcase_t &tc, bool expert_mode, parser_context_t ctx parser_error_t err; uint8_t buffer[5000]; - uint16_t bufferLen = parseHexString(buffer, sizeof(buffer), tc.blob.c_str()); + const uint16_t bufferLen = parseHexString(buffer, sizeof(buffer), tc.blob.c_str()); parser_tx_t tx_obj; memset(&tx_obj, 0, sizeof(tx_obj)); From 5ae2117a077a49985ac5315a7ef119d7b2fe0a49 Mon Sep 17 00:00:00 2001 From: Carlos Medeiros Date: Fri, 26 Jan 2024 17:31:25 +0000 Subject: [PATCH 2/4] format signature in rsv format --- app/src/crypto.c | 42 ++++++++++++++++----------- js/src/index.ts | 18 ++++++++---- js/src/types.ts | 4 ++- tests_zemu/tests/eth_legacy.test.ts | 2 +- tests_zemu/tests/eth_tests.test.ts | 6 ++-- tests_zemu/tests/transactions.test.ts | 35 ++++++++++++++++++---- 6 files changed, 73 insertions(+), 34 deletions(-) diff --git a/app/src/crypto.c b/app/src/crypto.c index 075e15d..56eada5 100644 --- a/app/src/crypto.c +++ b/app/src/crypto.c @@ -70,6 +70,17 @@ __Z_INLINE zxerr_t compressPubkey(const uint8_t *pubkey, uint16_t pubkeyLen, uin return zxerr_ok; } +typedef struct { + uint8_t r[32]; + uint8_t s[32]; + uint8_t v; + + // DER signature max size should be 73 + // https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature#77192 + uint8_t der_signature[73]; + +} __attribute__((packed)) signature_t; + zxerr_t crypto_sign(uint8_t *signature, uint16_t signatureMaxlen, uint16_t *sigSize, bool hash) { if (signature == NULL || sigSize == NULL) { return zxerr_invalid_crypto_settings; @@ -89,7 +100,10 @@ zxerr_t crypto_sign(uint8_t *signature, uint16_t signatureMaxlen, uint16_t *sigS cx_ecfp_private_key_t cx_privateKey = {0}; uint8_t privateKeyData[64] = {0}; unsigned int info = 0; - size_t signatureLength = MAX_DER_SIGNATURE_LEN; + uint32_t signatureLength = sizeof_field(signature_t, der_signature); + signature_t *const signature_object = (signature_t *)(signature); + *sigSize = 0; + zxerr_t error = zxerr_unknown; // Generate keys @@ -99,33 +113,27 @@ zxerr_t crypto_sign(uint8_t *signature, uint16_t signatureMaxlen, uint16_t *sigS // Sign CATCH_CXERROR(cx_ecdsa_sign_no_throw(&cx_privateKey, CX_RND_RFC6979 | CX_LAST, CX_SHA256, messageDigest, CX_SHA256_SIZE, - signature, &signatureLength, &info)); + signature_object->der_signature, &signatureLength, &info)); - *sigSize = signatureLength; - error = zxerr_ok; + const err_convert_e err_c = convertDERtoRSV(signature_object->der_signature, info, signature_object->r, + signature_object->s, &signature_object->v); + if (err_c != no_error) { + error = zxerr_unknown; + } else { + *sigSize = + sizeof_field(signature_t, r) + sizeof_field(signature_t, s) + sizeof_field(signature_t, v) + signatureLength; + error = zxerr_ok; + } catch_cx_error: MEMZERO(&cx_privateKey, sizeof(cx_privateKey)); MEMZERO(privateKeyData, sizeof(privateKeyData)); - if (error != zxerr_ok) { MEMZERO(signature, signatureMaxlen); } - return error; } -typedef struct { - uint8_t r[32]; - uint8_t s[32]; - uint8_t v; - - // DER signature max size should be 73 - // https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature#77192 - uint8_t der_signature[73]; - -} __attribute__((packed)) signature_t; - zxerr_t _sign(uint8_t *output, uint16_t outputLen, const uint8_t *message, uint16_t messageLen, uint16_t *sigSize, unsigned int *info) { if (output == NULL || message == NULL || sigSize == NULL || outputLen < sizeof(signature_t) || diff --git a/js/src/index.ts b/js/src/index.ts index 69f2b33..9416660 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -96,7 +96,9 @@ export default class FlareApp extends GenericApp { const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; let errorMessage = errorCodeToString(returnCode); - let signature = Buffer.alloc(0); + let r = Buffer.alloc(0); + let s = Buffer.alloc(0); + let v = Buffer.alloc(0); if ( returnCode === LedgerError.BadKeyHandle || @@ -106,10 +108,14 @@ export default class FlareApp extends GenericApp { errorMessage = `${errorMessage} : ${response.subarray(0, response.length - 2).toString("ascii")}`; } - if (returnCode === LedgerError.NoErrors && response.length > 2) { - signature = response.slice(0, response.length - 2); + if (returnCode === LedgerError.NoErrors && response.length >= 65) { + r = response.subarray(0, 32); + s = response.subarray(32, 64); + v = response.subarray(64, 65); return { - signature, + r, + s, + v, returnCode, errorMessage, }; @@ -164,11 +170,11 @@ export default class FlareApp extends GenericApp { }, processErrorResponse); } - async signETHTransaction(path: any, rawTxHex: any, resolution?: LedgerEthTransactionResolution | null) { + async signEVMTransaction(path: any, rawTxHex: any, resolution?: LedgerEthTransactionResolution | null) { return this.eth.signTransaction(path, rawTxHex, resolution); } - async getETHAddress(path: any, boolDisplay: any, boolChaincode?: boolean) { + async getEVMAddress(path: any, boolDisplay: any, boolChaincode?: boolean) { return this.eth.getAddress(path, boolDisplay, boolChaincode); } } diff --git a/js/src/types.ts b/js/src/types.ts index 6713285..838442c 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -13,5 +13,7 @@ export interface ResponseAddress extends ResponseBase { } export interface ResponseSign extends ResponseBase { - signature?: Buffer; + r?: Buffer; + s?: Buffer; + v?: Buffer; } diff --git a/tests_zemu/tests/eth_legacy.test.ts b/tests_zemu/tests/eth_legacy.test.ts index 803a6ac..005dd3b 100644 --- a/tests_zemu/tests/eth_legacy.test.ts +++ b/tests_zemu/tests/eth_legacy.test.ts @@ -137,7 +137,7 @@ describe.each(models)('ETH_Legacy', function (m) { const msg = rawUnsignedLegacyTransaction(data.op, data.chainId) console.log('tx: ', msg.toString('hex')) - const respReq = app.signETHTransaction(ETH_PATH, msg.toString('hex'), null) + const respReq = app.signEVMTransaction(ETH_PATH, msg.toString('hex'), null) await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) await sim.compareSnapshotsAndApprove('.', `${m.prefix.toLowerCase()}-eth-${data.name}`) diff --git a/tests_zemu/tests/eth_tests.test.ts b/tests_zemu/tests/eth_tests.test.ts index 34bad1a..9f5c360 100644 --- a/tests_zemu/tests/eth_tests.test.ts +++ b/tests_zemu/tests/eth_tests.test.ts @@ -100,7 +100,7 @@ describe.each(models)('ETH', function (m) { const EXPECTED_PUBLIC_KEY = '024f1dd50f180bfd546339e75410b127331469837fa618d950f7cfb8be351b0020' // do not wait here.. - const signatureRequest = app.signETHTransaction(ETH_PATH, msg, null) + const signatureRequest = app.signEVMTransaction(ETH_PATH, msg, null) // Wait until we are not in the main menu await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) @@ -134,7 +134,7 @@ describe('EthAddress', function () { await sim.start({ ...defaultOptions, model: m.name }) const app = new FilecoinApp(sim.getTransport()) - const resp = await app.getETHAddress(ETH_PATH, false, true) + const resp = await app.getEVMAddress(ETH_PATH, false, true) console.log(resp) @@ -159,7 +159,7 @@ describe('EthAddress', function () { }) const app = new FilecoinApp(sim.getTransport()) - const resp = app.getETHAddress(ETH_PATH, true) + const resp = app.getEVMAddress(ETH_PATH, true) await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) diff --git a/tests_zemu/tests/transactions.test.ts b/tests_zemu/tests/transactions.test.ts index 08c904d..264bafd 100644 --- a/tests_zemu/tests/transactions.test.ts +++ b/tests_zemu/tests/transactions.test.ts @@ -20,6 +20,7 @@ import { models, hdpath, defaultOptions } from './common' import secp256k1 from 'secp256k1' import { createHash } from 'crypto' import { sha256 } from 'js-sha256' +import { ec } from 'elliptic' const TEST_DATA = [ { @@ -127,13 +128,20 @@ describe.each(models)('Transactions', function (m) { const signatureResponse = await signatureRequest console.log(signatureResponse) + expect(signatureResponse).toHaveProperty('s') + expect(signatureResponse).toHaveProperty('r') + expect(signatureResponse).toHaveProperty('v') expect(signatureResponse.returnCode).toEqual(0x9000) expect(signatureResponse.errorMessage).toEqual('No errors') + const EC = new ec('secp256k1') + const signature_obj = { + r: signatureResponse.r!, + s: signatureResponse.s!, + } // Now verify the signature const message = createHash('sha256').update(data.blob).digest() - const signature = new Uint8Array(signatureResponse.signature!) - const valid = secp256k1.ecdsaVerify(secp256k1.signatureImport(signature), new Uint8Array(message), pubKey) + const valid = EC.verify(message, signature_obj, Buffer.from(pubKey), 'hex') expect(valid).toEqual(true) } finally { await sim.close() @@ -166,13 +174,20 @@ describe.each(models)('Transactions', function (m) { const signatureResponse = await signatureRequest console.log(signatureResponse) + expect(signatureResponse).toHaveProperty('s') + expect(signatureResponse).toHaveProperty('r') + expect(signatureResponse).toHaveProperty('v') expect(signatureResponse.returnCode).toEqual(0x9000) expect(signatureResponse.errorMessage).toEqual('No errors') + const EC = new ec('secp256k1') + const signature_obj = { + r: signatureResponse.r!, + s: signatureResponse.s!, + } // Now verify the signature const message = createHash('sha256').update(data.blob).digest() - const signature = new Uint8Array(signatureResponse.signature!) - const valid = secp256k1.ecdsaVerify(secp256k1.signatureImport(signature), new Uint8Array(message), pubKey) + const valid = EC.verify(message, signature_obj, Buffer.from(pubKey), 'hex') expect(valid).toEqual(true) } finally { await sim.close() @@ -201,12 +216,20 @@ describe.each(models)('Transactions', function (m) { const signatureResponse = await signatureRequest console.log(signatureResponse) + + expect(signatureResponse).toHaveProperty('s') + expect(signatureResponse).toHaveProperty('r') + expect(signatureResponse).toHaveProperty('v') expect(signatureResponse.returnCode).toEqual(0x9000) expect(signatureResponse.errorMessage).toEqual('No errors') + const EC = new ec('secp256k1') + const signature_obj = { + r: signatureResponse.r!, + s: signatureResponse.s!, + } // Now verify the signature - const signature = new Uint8Array(signatureResponse.signature!) - const valid = secp256k1.ecdsaVerify(secp256k1.signatureImport(signature), msg, pubKey) + const valid = EC.verify(msg, signature_obj, Buffer.from(pubKey), 'hex') expect(valid).toEqual(true) } finally { await sim.close() From 02bf884a1305c8ff3f009070f3bb59d206a204ba Mon Sep 17 00:00:00 2001 From: Carlos Medeiros Date: Fri, 26 Jan 2024 17:31:42 +0000 Subject: [PATCH 3/4] bump version and update snapshots --- app/Makefile.version | 2 +- tests_zemu/snapshots/s-mainmenu/00004.png | Bin 375 -> 369 bytes tests_zemu/snapshots/s-mainmenu/00010.png | Bin 375 -> 369 bytes tests_zemu/snapshots/sp-mainmenu/00004.png | Bin 328 -> 317 bytes tests_zemu/snapshots/sp-mainmenu/00010.png | Bin 328 -> 317 bytes tests_zemu/snapshots/st-mainmenu/00001.png | Bin 13880 -> 13974 bytes tests_zemu/snapshots/x-mainmenu/00004.png | Bin 328 -> 317 bytes tests_zemu/snapshots/x-mainmenu/00010.png | Bin 328 -> 317 bytes 8 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Makefile.version b/app/Makefile.version index 0d4abfb..f86d41b 100644 --- a/app/Makefile.version +++ b/app/Makefile.version @@ -3,4 +3,4 @@ APPVERSION_M=0 # This is the minor version APPVERSION_N=0 # This is the patch version -APPVERSION_P=7 +APPVERSION_P=8 diff --git a/tests_zemu/snapshots/s-mainmenu/00004.png b/tests_zemu/snapshots/s-mainmenu/00004.png index 25444c8cf59ca12c2d10962a251c52900dfa3e1d..0a432139b1aad4ceac45ef2e6ba3ee4ff55dd2c1 100644 GIT binary patch delta 342 zcmV-c0jd7?0`UTnB!4(bL_t(|ob8#x4ul{KM5|f<|ARfa2Mj5t2ncBq=4B7Y?aDfB z2T&pa006798I5zD;jqVjW;=}jz^MF8M_XR*7~2G-;Qir~be*zM)Uc1Un~rv!(u*A1 zaepXOIX+gyzRVaF^q{7Ydlub2WeKRg?J9prnN0hYzksa;y?H`%`eR{JBDjckk?E0!bU~KAFK^<>0`*^24V0bDM;O?_F z2V(0q*G{UD%1;D1;odnC|81`}61(UnENH0WUCx3)Z>$o)D>Q2q*-2ueS;Xq4JS860 orCvr7DXvl;00000007434@Sb9>d5OziU0rr07*qoM6N<$f;)qp*8l(j delta 348 zcmV-i0i*u$0`~%tB!50hL_t(|ob8$24uc>Jh0)CSe_${40z*ncp~@Nx=gTgP>qJg} zD9kwk006VHD2;RM@vz%Kq`Qwx~Yb6`Ai(wt#L4dVkGux(7ONK#2Y4@TX=2 z6{5O*KxO+8%*GAa=CsRzfud5A+#>87@H-&1lvGc0C&>W1{Q*s2ddB{U*8ut*yQYt0 zq~5|BkP;$07H8!afovYss1LH_)OT;Xpvu82DfU;QWD}tS z{8XhAW?QGRc2!az5jwyN_pTz*-&&5@w#Z9Rhrw&vsU4gJ&5f1=Xca1kYH^VmXcejZ uKj2TH`%`eR{JBDjckk?E0!bU~KAFK^<>0`*^24V0bDM;O?_F z2V(0q*G{UD%1;D1;odnC|81`}61(UnENH0WUCx3)Z>$o)D>Q2q*-2ueS;Xq4JS860 orCvr7DXvl;00000007434@Sb9>d5OziU0rr07*qoM6N<$f;)qp*8l(j delta 348 zcmV-i0i*u$0`~%tB!50hL_t(|ob8$24uc>Jh0)CSe_${40z*ncp~@Nx=gTgP>qJg} zD9kwk006VHD2;RM@vz%Kq`Qwx~Yb6`Ai(wt#L4dVkGux(7ONK#2Y4@TX=2 z6{5O*KxO+8%*GAa=CsRzfud5A+#>87@H-&1lvGc0C&>W1{Q*s2ddB{U*8ut*yQYt0 zq~5|BkP;$07H8!afovYss1LH_)OT;Xpvu82DfU;QWD}tS z{8XhAW?QGRc2!az5jwyN_pTz*-&&5@w#Z9Rhrw&vsU4gJ&5f1=Xca1kYH^VmXcejZ uKj2TSa5AiX&fD<>1do6tc_AJ*&8>Cum zw@@~3i}F6YfD^UWt{2V9m+5=T>*LMaa-^?-y=i-pW*2v6?te}C0BqW8GMh3xPm|t) zE(_1S=?5Tu!Oq&;KdyFpTgdl1DI=uHd)p0J`cJbFwe+9N7>uL~jpTQDq}Pu3;NRr6 zOZ>vYtA%TqLL@#3cVWe5cW3LFdwH6Nx1huo@Q3Cdg0@Kz=;i~EFZ6ZvU3Y+A$+~xU oXS(AbUIG9B02=@R0091pPnsLSa)uoAQvd(}07*qoM6N<$f@ac(wg3PC delta 301 zcmdnXbb@JuNbL*{0vu43yT@8G^X9MA9>ckvhE|~^9n;GhrPaE3FT2+;@9VD<|&RQz*@DxbY)78&qol`;+0K}b#b^rhX diff --git a/tests_zemu/snapshots/sp-mainmenu/00010.png b/tests_zemu/snapshots/sp-mainmenu/00010.png index f033b5be8ff06bb10547d286587afc00b1935343..0e0c3d04c888eecc57c945ffac25c9f0163ea8be 100644 GIT binary patch delta 290 zcmV+-0p0${0=)u|B!2`+L_t(|obB05a)U4s1wfL@O?3Yw=`LANQ4#S=sMvZY-75?% z#!u4NplJdC0000000181ZFB*Sa5AiX&fD<>1do6tc_AJ*&8>Cum zw@@~3i}F6YfD^UWt{2V9m+5=T>*LMaa-^?-y=i-pW*2v6?te}C0BqW8GMh3xPm|t) zE(_1S=?5Tu!Oq&;KdyFpTgdl1DI=uHd)p0J`cJbFwe+9N7>uL~jpTQDq}Pu3;NRr6 zOZ>vYtA%TqLL@#3cVWe5cW3LFdwH6Nx1huo@Q3Cdg0@Kz=;i~EFZ6ZvU3Y+A$+~xU oXS(AbUIG9B02=@R0091pPnsLSa)uoAQvd(}07*qoM6N<$f@ac(wg3PC delta 301 zcmdnXbb@JuNbL*{0vu43yT@8G^X9MA9>ckvhE|~^9n;GhrPaE3FT2+;@9VD<|&RQz*@DxbY)78&qol`;+0K}b#b^rhX diff --git a/tests_zemu/snapshots/st-mainmenu/00001.png b/tests_zemu/snapshots/st-mainmenu/00001.png index f4abe19620188e558ba8fcd0a7c8a6189fdcae81..37dc858ba492ed4e48e41b0d37e4c8f3cdb968ed 100644 GIT binary patch literal 13974 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 literal 13880 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 jW8(4uq|qM#i~YpFg7z&>q|8kAou?dr6}RL diff --git a/tests_zemu/snapshots/x-mainmenu/00004.png b/tests_zemu/snapshots/x-mainmenu/00004.png index f033b5be8ff06bb10547d286587afc00b1935343..0e0c3d04c888eecc57c945ffac25c9f0163ea8be 100644 GIT binary patch delta 290 zcmV+-0p0${0=)u|B!2`+L_t(|obB05a)U4s1wfL@O?3Yw=`LANQ4#S=sMvZY-75?% z#!u4NplJdC0000000181ZFB*Sa5AiX&fD<>1do6tc_AJ*&8>Cum zw@@~3i}F6YfD^UWt{2V9m+5=T>*LMaa-^?-y=i-pW*2v6?te}C0BqW8GMh3xPm|t) zE(_1S=?5Tu!Oq&;KdyFpTgdl1DI=uHd)p0J`cJbFwe+9N7>uL~jpTQDq}Pu3;NRr6 zOZ>vYtA%TqLL@#3cVWe5cW3LFdwH6Nx1huo@Q3Cdg0@Kz=;i~EFZ6ZvU3Y+A$+~xU oXS(AbUIG9B02=@R0091pPnsLSa)uoAQvd(}07*qoM6N<$f@ac(wg3PC delta 301 zcmdnXbb@JuNbL*{0vu43yT@8G^X9MA9>ckvhE|~^9n;GhrPaE3FT2+;@9VD<|&RQz*@DxbY)78&qol`;+0K}b#b^rhX diff --git a/tests_zemu/snapshots/x-mainmenu/00010.png b/tests_zemu/snapshots/x-mainmenu/00010.png index f033b5be8ff06bb10547d286587afc00b1935343..0e0c3d04c888eecc57c945ffac25c9f0163ea8be 100644 GIT binary patch delta 290 zcmV+-0p0${0=)u|B!2`+L_t(|obB05a)U4s1wfL@O?3Yw=`LANQ4#S=sMvZY-75?% z#!u4NplJdC0000000181ZFB*Sa5AiX&fD<>1do6tc_AJ*&8>Cum zw@@~3i}F6YfD^UWt{2V9m+5=T>*LMaa-^?-y=i-pW*2v6?te}C0BqW8GMh3xPm|t) zE(_1S=?5Tu!Oq&;KdyFpTgdl1DI=uHd)p0J`cJbFwe+9N7>uL~jpTQDq}Pu3;NRr6 zOZ>vYtA%TqLL@#3cVWe5cW3LFdwH6Nx1huo@Q3Cdg0@Kz=;i~EFZ6ZvU3Y+A$+~xU oXS(AbUIG9B02=@R0091pPnsLSa)uoAQvd(}07*qoM6N<$f@ac(wg3PC delta 301 zcmdnXbb@JuNbL*{0vu43yT@8G^X9MA9>ckvhE|~^9n;GhrPaE3FT2+;@9VD<|&RQz*@DxbY)78&qol`;+0K}b#b^rhX From 42d85e0e9f9af3e3007aa52c819caf85c94b64d2 Mon Sep 17 00:00:00 2001 From: Carlos Medeiros Date: Fri, 26 Jan 2024 17:31:51 +0000 Subject: [PATCH 4/4] add extra checks --- app/src/eth_utils.c | 2 +- app/src/parser_impl_eth.c | 1 + app/src/parser_impl_eth.h | 1 - app/src/uint256.c | 104 +++++++++++++++++--------------------- 4 files changed, 48 insertions(+), 60 deletions(-) diff --git a/app/src/eth_utils.c b/app/src/eth_utils.c index 87e9c06..b38137d 100644 --- a/app/src/eth_utils.c +++ b/app/src/eth_utils.c @@ -139,7 +139,7 @@ parser_error_t printRLPNumber(const rlp_t *num, char *outVal, uint16_t outValLen } parser_error_t printEVMAddress(const rlp_t *address, char *outVal, uint16_t outValLen, uint8_t pageIdx, uint8_t *pageCount) { - if (address == NULL || outVal == NULL || pageCount == NULL || address->rlpLen != ETH_ADDR_LEN) { + if (address == NULL || outVal == NULL || address->ptr == NULL || pageCount == NULL || address->rlpLen != ETH_ADDR_LEN) { return parser_unexpected_error; } diff --git a/app/src/parser_impl_eth.c b/app/src/parser_impl_eth.c index 45b80a8..48f7684 100644 --- a/app/src/parser_impl_eth.c +++ b/app/src/parser_impl_eth.c @@ -138,6 +138,7 @@ static parser_error_t readTxnType(parser_context_t *ctx, eth_tx_type_e *type) { } parser_error_t _readEth(parser_context_t *ctx, eth_tx_t *tx_obj) { + MEMZERO(ð_tx_obj, sizeof(eth_tx_obj)); CHECK_ERROR(readTxnType(ctx, &tx_obj->tx_type)) // We expect a list with all the fields from the transaction rlp_t list = {0}; diff --git a/app/src/parser_impl_eth.h b/app/src/parser_impl_eth.h index ac0f2db..ef956cb 100644 --- a/app/src/parser_impl_eth.h +++ b/app/src/parser_impl_eth.h @@ -73,7 +73,6 @@ parser_error_t _getNumItemsEth(uint8_t *numItems); parser_error_t _validateTxEth(); -// parser_error_t _computeV(unsigned int info, uint8_t *v); parser_error_t _computeV(parser_context_t *ctx, eth_tx_t *tx_obj, unsigned int info, uint8_t *v); #ifdef __cplusplus diff --git a/app/src/uint256.c b/app/src/uint256.c index 6fc3482..f1b0035 100644 --- a/app/src/uint256.c +++ b/app/src/uint256.c @@ -1,28 +1,27 @@ /******************************************************************************* -* Ledger Ethereum App -* (c) 2016-2019 Ledger -* -* 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. -********************************************************************************/ + * Ledger Ethereum App + * (c) 2016-2019 Ledger + * + * 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. + ********************************************************************************/ // Adapted from https://github.com/calccrypto/uint256_t -#include -#include -#include - #include "uint256.h" +#include +#include +#include static const char HEXDIGITS[] = "0123456789abcdef"; @@ -61,13 +60,9 @@ parser_error_t readu256BE(parser_context_t *ctx, uint256_t *bigInt) { return parser_ok; } -bool zero128(uint128_t *number) { - return ((LOWER_P(number) == 0) && (UPPER_P(number) == 0)); -} +bool zero128(uint128_t *number) { return ((LOWER_P(number) == 0) && (UPPER_P(number) == 0)); } -bool zero256(uint256_t *number) { - return (zero128(&LOWER_P(number)) && zero128(&UPPER_P(number))); -} +bool zero256(uint256_t *number) { return (zero128(&LOWER_P(number)) && zero128(&UPPER_P(number))); } void copy128(uint128_t *target, uint128_t *number) { UPPER_P(target) = UPPER_P(number); @@ -98,8 +93,7 @@ void shiftl128(uint128_t *number, uint32_t value, uint128_t *target) { } else if (value == 0) { copy128(target, number); } else if (value < 64) { - UPPER_P(target) = - (UPPER_P(number) << value) + (LOWER_P(number) >> (64 - value)); + UPPER_P(target) = (UPPER_P(number) << value) + (LOWER_P(number) >> (64 - value)); LOWER_P(target) = (LOWER_P(number) << value); } else if (value > 64) { UPPER_P(target) = LOWER_P(number) << (value - 64); @@ -143,8 +137,7 @@ void shiftr128(uint128_t *number, uint32_t value, uint128_t *target) { } else if (value < 64) { uint128_t result; UPPER(result) = UPPER_P(number) >> value; - LOWER(result) = - (UPPER_P(number) << (64 - value)) + (LOWER_P(number) >> value); + LOWER(result) = (UPPER_P(number) << (64 - value)) + (LOWER_P(number) >> value); copy128(target, &result); } else if (value > 64) { LOWER_P(target) = UPPER_P(number) >> (value - 64); @@ -220,13 +213,11 @@ uint32_t bits256(uint256_t *number) { } bool equal128(uint128_t *number1, uint128_t *number2) { - return (UPPER_P(number1) == UPPER_P(number2)) && - (LOWER_P(number1) == LOWER_P(number2)); + return (UPPER_P(number1) == UPPER_P(number2)) && (LOWER_P(number1) == LOWER_P(number2)); } bool equal256(uint256_t *number1, uint256_t *number2) { - return (equal128(&UPPER_P(number1), &UPPER_P(number2)) && - equal128(&LOWER_P(number1), &LOWER_P(number2))); + return (equal128(&UPPER_P(number1), &UPPER_P(number2)) && equal128(&LOWER_P(number1), &LOWER_P(number2))); } bool gt128(uint128_t *number1, uint128_t *number2) { @@ -243,18 +234,12 @@ bool gt256(uint256_t *number1, uint256_t *number2) { return gt128(&UPPER_P(number1), &UPPER_P(number2)); } -bool gte128(uint128_t *number1, uint128_t *number2) { - return gt128(number1, number2) || equal128(number1, number2); -} +bool gte128(uint128_t *number1, uint128_t *number2) { return gt128(number1, number2) || equal128(number1, number2); } -bool gte256(uint256_t *number1, uint256_t *number2) { - return gt256(number1, number2) || equal256(number1, number2); -} +bool gte256(uint256_t *number1, uint256_t *number2) { return gt256(number1, number2) || equal256(number1, number2); } void add128(uint128_t *number1, uint128_t *number2, uint128_t *target) { - UPPER_P(target) = - UPPER_P(number1) + UPPER_P(number2) + - ((LOWER_P(number1) + LOWER_P(number2)) < LOWER_P(number1)); + UPPER_P(target) = UPPER_P(number1) + UPPER_P(number2) + ((LOWER_P(number1) + LOWER_P(number2)) < LOWER_P(number1)); LOWER_P(target) = LOWER_P(number1) + LOWER_P(number2); } @@ -272,9 +257,7 @@ void add256(uint256_t *number1, uint256_t *number2, uint256_t *target) { } void minus128(uint128_t *number1, uint128_t *number2, uint128_t *target) { - UPPER_P(target) = - UPPER_P(number1) - UPPER_P(number2) - - ((LOWER_P(number1) - LOWER_P(number2)) > LOWER_P(number1)); + UPPER_P(target) = UPPER_P(number1) - UPPER_P(number2) - ((LOWER_P(number1) - LOWER_P(number2)) > LOWER_P(number1)); LOWER_P(target) = LOWER_P(number1) - LOWER_P(number2); } @@ -302,10 +285,9 @@ void or256(uint256_t *number1, uint256_t *number2, uint256_t *target) { } void mul128(uint128_t *number1, uint128_t *number2, uint128_t *target) { - uint64_t top[4] = {UPPER_P(number1) >> 32, UPPER_P(number1) & 0xffffffff, - LOWER_P(number1) >> 32, LOWER_P(number1) & 0xffffffff}; - uint64_t bottom[4] = {UPPER_P(number2) >> 32, UPPER_P(number2) & 0xffffffff, - LOWER_P(number2) >> 32, + uint64_t top[4] = {UPPER_P(number1) >> 32, UPPER_P(number1) & 0xffffffff, LOWER_P(number1) >> 32, + LOWER_P(number1) & 0xffffffff}; + uint64_t bottom[4] = {UPPER_P(number2) >> 32, UPPER_P(number2) & 0xffffffff, LOWER_P(number2) >> 32, LOWER_P(number2) & 0xffffffff}; uint64_t products[4][4]; uint128_t tmp, tmp2; @@ -437,8 +419,7 @@ void mul256(uint256_t *number1, uint256_t *number2, uint256_t *target) { add256(&target1, &target2, target); } -void divmod128(uint128_t *l, uint128_t *r, uint128_t *retDiv, - uint128_t *retMod) { +void divmod128(uint128_t *l, uint128_t *r, uint128_t *retDiv, uint128_t *retMod) { uint128_t copyd, adder, resDiv, resMod; uint128_t one; UPPER(one) = 0; @@ -469,8 +450,7 @@ void divmod128(uint128_t *l, uint128_t *r, uint128_t *retDiv, } } -void divmod256(uint256_t *l, uint256_t *r, uint256_t *retDiv, - uint256_t *retMod) { +void divmod256(uint256_t *l, uint256_t *r, uint256_t *retDiv, uint256_t *retMod) { uint256_t copyd, adder, resDiv, resMod; uint256_t one; clear256(&one); @@ -512,8 +492,11 @@ static void reverseString(char *str, uint32_t length) { } } -bool tostring128(uint128_t *number, uint32_t baseParam, char *out, - uint32_t outLength) { +bool tostring128(uint128_t *number, uint32_t baseParam, char *out, uint32_t outLength) { + if (number == NULL || out == NULL || outLength == 0) { + return false; + } + uint128_t rDiv; uint128_t rMod; uint128_t base; @@ -532,13 +515,18 @@ bool tostring128(uint128_t *number, uint32_t baseParam, char *out, divmod128(&rDiv, &base, &rDiv, &rMod); out[offset++] = HEXDIGITS[(uint8_t)LOWER(rMod)]; } while (!zero128(&rDiv)); + if (offset >= outLength) { + return false; + } out[offset] = '\0'; reverseString(out, offset); return true; } -bool tostring256(uint256_t *number, uint32_t baseParam, char *out, - uint32_t outLength) { +bool tostring256(uint256_t *number, uint32_t baseParam, char *out, uint32_t outLength) { + if (number == NULL || out == NULL || outLength <= 1) { + return false; + } uint256_t rDiv; uint256_t rMod; uint256_t base; @@ -552,7 +540,7 @@ bool tostring256(uint256_t *number, uint32_t baseParam, char *out, return false; } - outLength--; // Keep a byte for termination + outLength--; // Keep a byte for termination do { if (offset > (outLength - 1)) {