diff --git a/crypto/Makefile b/crypto/Makefile index e9fc3867e..81e24e5b9 100644 --- a/crypto/Makefile +++ b/crypto/Makefile @@ -2,10 +2,14 @@ ifeq ($(FUZZER),1) CC ?= clang LD ?= $(CC) SANFLAGS += -fsanitize=fuzzer -CFLAGS += -fsanitize-ignorelist=fuzzer/sanitizer_ignorelist.txt -# this works around clang optimization issues in relation with -fsanitize=undefined +# TODO gcc as well as older clang versions <= 12 do not support this feature +$(info "info: using -fsanitize-ignorelist") +SANFLAGS += -fsanitize-ignorelist=fuzzer/sanitizer_ignorelist.txt + # TODO is there a better solution, for example by disabling a specific optimization technique? +# there is a clang optimization issue in relation with the blake2 code at -fsanitize=undefined +$(warning "warning: disable optimization on blake2 code as workaround") blake2b.o: OPTFLAGS += -O0 blake2s.o: OPTFLAGS += -O0 @@ -56,6 +60,12 @@ ZKP_CFLAGS = \ ZKP_PATH = ../vendor/secp256k1-zkp CFLAGS += -DSECP256K1_CONTEXT_SIZE=208 +# TODO remove this workaround once possible +ifeq ($(CC),clang-14) +$(warning "warning: suppress clang-14 compiler warning for secp256k1-zkp code") +ZKP_CFLAGS += -Wno-bitwise-instead-of-logical +endif + VALGRIND ?= 1 ifeq ($(VALGRIND),1) CFLAGS += -DVALGRIND diff --git a/crypto/fuzzer/README.md b/crypto/fuzzer/README.md index d9bb37243..e7f2f1f00 100644 --- a/crypto/fuzzer/README.md +++ b/crypto/fuzzer/README.md @@ -47,6 +47,9 @@ To be determined: * `-fstack-protector-strong` or `-fstack-protector-all` * `-m32` to closer evaluate the 32 bit behavior * this requires 32bit build support for gcc-multilib, libc and others + * adjust Makefile to `CFLAGS += -DSECP256K1_CONTEXT_SIZE=192` +* `-DSHA2_UNROLL_TRANSFORM` SHA2 optimization flags +* `-fsanitize-coverage=edge,trace-cmp,trace-div,indirect-calls,trace-gep,no-prune` to add program counter granularity ## Operation diff --git a/crypto/fuzzer/extract_fuzzer_dictionary.py b/crypto/fuzzer/extract_fuzzer_dictionary.py new file mode 100755 index 000000000..482596be3 --- /dev/null +++ b/crypto/fuzzer/extract_fuzzer_dictionary.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This experimental program is designed to extract a subset of interesting test +case snippets from the trezor-crypto test directory and output them as a +standard fuzzer dictionary file. + +The program is built on quick-and-dirty regex matching that is known to be +incorrect for parsing code files, but is considered "good enough" for this +specific purpose. +Note that there are target-specific configurations and internal filter settings. +""" + +import argparse +import binascii +import glob +import re + +# re2 is considered for future use +# it requires a system installation and the google-re2 python package +# import re2 + + +# Expected target format for strings in code: +# Most strings are defined in the general form "example" +# There are a few test vectors in crypto/tests/wycheproof/javascript/EcUtil.js +# with 'example' style string definitions, these are ignored for now + +TARGET_DIR = "../tests" + +# intentionally excluded file types that currently do not provide enough value: +# *.js, *.md, *.sh, *.html and others from the wycheproof subdirectory + +targeted_filetypes_multiline_classA = ("*.c", "*.h", "*.py") +# Java files have different multiline strings that are handled differently +targeted_filetypes_multiline_classB = ("*.java",) +targeted_filetypes_multiline = ( + targeted_filetypes_multiline_classA + targeted_filetypes_multiline_classB +) + +# files without multiline string content +# Note: consider switching to actual JSON parsing? +# Note: the wycheproof repository has a number of test cases for other +# cryptography such as DSA and RSA which are currently less interesting for the +# fuzzer dictionary and therefore excluded +targeted_filetypes_singleline = ( + "aes*.json", + "ecdh*.json", + "ecdsa*.json", + "x25519*.json", + "chacha20*.json", + "kw*.json", +) + +verbose = False + +# patterns to extract +# singleline: +# "4a1e76f133afb" +# 0xAF8BBDFE8CDD5 and 0x0488b21e +# m/0'/2147483647'/1'/2147483646'/2' in test_check.c via m/[\d'/]+ +# +# multiline: +# "fffc" \n "99" +# "dpubZ9169K" \n "bTYbcY" +# "\x65\xf9" \\n "\xa0\x6a" +# { 0x086d8bd5, 0x1018f82f, \n 0xc55ece} , see rg "0x([a-zA-Z0-9])+" + +# patterns to ignore +# lines with print statements +# lines with exceptions +# comments and other metadata in the testvector JSON files +# filenames +# import statements and other package names + +# patterns to investigate further +# public keys with the form BEGIN PUBLIC KEY +# TODO "abc" + "def" string concatenation on the same line without newline +# strings in comments + +# TODO briefly describe the desired dictionary export file format and its quirks + +# match everything in quotes that doesn't have an internal quote character and +# at least one internal character +regex_string_general_definition = r"\"[^\"]+\"" +regex_string_general = re.compile(regex_string_general_definition) +# the capturing group ignores prefix and suffix outside of the quotes +# Note that this is prone to matching the last line of a C-style multiline string, +# which is addressed via extra state handling during the file processing +regex_oneline_string = re.compile( + r"(" + regex_string_general_definition + r")\s*[\,\)]+" +) +# ignore lines that have a "+" character preceding a string +regex_oneline_string_java_ignore1 = re.compile(r"^\s*\+\s*\"") + +regex_hex_character_segment_inner_definition = "[0-9a-fA-F]+" +regex_hex_character_input_complete = re.compile( + '^"' + regex_hex_character_segment_inner_definition + '"$' +) +regex_hex_character_input_inner = re.compile( + regex_hex_character_segment_inner_definition +) +# most constants are preceded by a space, but some have a "(" "[" or "{" before them +regex_hex_constant_singleline = re.compile(r"(?<=\(|\[|\{| )0x[a-fA-F0-9]+") + +regex_c_style_multiline = re.compile(r"(?:\".+\"\s*\n\s*)+(?:\".+\")", re.MULTILINE) +regex_c_intermediary_content = re.compile(r"\"\s*\n\s*\"", re.MULTILINE) +# TODO how to prevent matching in the middle of a multi-line string concatenation? +# negative lookbehind for "+" is not possible generically and +# (? -1: + return False + return True + + +def ignore_single_line_java(data): + """return True if the input should be ignored""" + for keyword in ignore_keywords_java: + if data.find(keyword) > -1: + return True + return False + + +def ignore_single_line_c(data): + """return True if the input should be ignored""" + for keyword in ignore_keywords_c: + if data.find(keyword) > -1: + return True + return False + + +def ignore_general(data): + """return True if the input should be ignored""" + if regex_filename_heuristic.search(data): + return True + return False + + +def encode_strings_for_dictionary(data): + """ + Assumes that inputs are already in string quotes + + Handles dictionary-specific encoding steps + """ + # libfuzzer does not like "\n" string patterns in dictionary files, replace + # it with an encoded newline + data = regex_text_newline.sub("\\\\x0a", data) + return data + + +def detect_and_convert_hex(data): + """ + Convert hex strings + + Directly pass through non-hex content + """ + global counter_hex_content + global counter_wycheproof_hex_reconstruction + match_result1 = regex_hex_character_input_complete.search(data) + if match_result1: + + match_result2 = regex_hex_character_input_inner.search(match_result1.string) + isolated_substring = match_result2.group(0) + if len(isolated_substring) % 2 == 1: + # Note: the test cases in the wycheproof testvector JSON files have + # a custom binary hex format to represent keys + # among other things, this results in hex strings with an uneven + # number of characters + # see tests/wycheproof/java/com/google/security/wycheproof/JsonUtil.java + # specifically the asBigInteger() function for more information + if isolated_substring[0] >= "0" and isolated_substring[0] <= "7": + isolated_substring = "0" + isolated_substring + else: + isolated_substring = "f" + isolated_substring + counter_wycheproof_hex_reconstruction += 1 + + converted_result = "" + try: + # test error-free conversion to binary + binascii.unhexlify(isolated_substring) + hex_with_c_style_formatting = "" + pos = 0 + while pos < len(isolated_substring) - 1: + hex_with_c_style_formatting += "\\x" + isolated_substring[pos : pos + 2] + pos += 2 + + converted_result = '"%s"' % hex_with_c_style_formatting + # TODO binascii.Incomplete exception also relevant? + except binascii.Error: + # default to the original input + return data + counter_hex_content += 1 + return converted_result + return data + + +def search_files_recursively(directory, filetype_glob): + """returns glob search results""" + target_files = [] + print_verbose("searching in %s" % directory) + for filetype in filetype_glob: + print_verbose("searching for %s" % filetype) + target_files.extend(glob.glob(f"{directory}/**/{filetype}", recursive=True)) + return target_files + + +def print_verbose(text): + """print wrapper""" + if verbose: + print(text) + + +def recursive_dictionary_extraction(directory): + """handle the central extraction logic""" + # TODO split this function up into subfunctions + global counter_hex_content + # handle as a set structure to de-duplicate results automatically + candidate_lines = set() + + target_files = search_files_recursively(directory, targeted_filetypes_singleline) + for filepath in target_files: + per_file_result_counter = 0 + with open(filepath) as _file: + print_verbose("processing %s" % filepath) + for _, line in enumerate(_file.readlines()): + if ignore_single_line_json(line): + continue + results = regex_oneline_string.findall(line) + for result in results: + candidate_lines.add(result) + per_file_result_counter += 1 + if per_file_result_counter > 0: + print_verbose("results: %d" % per_file_result_counter) + + print_verbose("number of candidate entries: %d" % len(candidate_lines)) + + target_files = search_files_recursively(directory, targeted_filetypes_multiline) + for filepath in target_files: + per_file_result_counter = 0 + with open(filepath) as _file: + last_line_was_multiline_string = False + print_verbose("processing %s for single-line strings" % filepath) + for _, line in enumerate(_file.readlines()): + if ignore_single_line_java(line): + last_line_was_multiline_string = False + continue + if ignore_single_line_c(line): + last_line_was_multiline_string = False + continue + if regex_oneline_string_java_ignore1.search(line): + last_line_was_multiline_string = True + if regex_oneline_string.search(line): + # the Java multiline string apparently ends on this line + last_line_was_multiline_string = False + continue + + result_general_string = regex_string_general.search(line) + if result_general_string: + # at least one general string is matched, see if it is + # a single-line string + results = regex_oneline_string.findall(line) + for result in results: + if not last_line_was_multiline_string: + candidate_lines.add(result) + per_file_result_counter += 1 + last_line_was_multiline_string = False + if len(results) == 0: + last_line_was_multiline_string = True + else: + last_line_was_multiline_string = False + + # TODO split this into a separate loop? + results = regex_hex_constant_singleline.findall(line) + for result in results: + # remove the "0x" prefix, add quotes + candidate_lines.add('"%s"' % result[2:]) + per_file_result_counter += 1 + + if per_file_result_counter > 0: + print_verbose("results: %d" % per_file_result_counter) + + target_files = search_files_recursively( + directory, targeted_filetypes_multiline_classA + ) + + for filepath in target_files: + with open(filepath) as _file: + print_verbose("processing %s for C-style multi-line strings" % filepath) + filecontent = _file.read() + multiline_results = regex_c_style_multiline.findall(filecontent) + if len(multiline_results) > 0: + print_verbose("results: %d" % len(multiline_results)) + for result in multiline_results: + cleanup = regex_c_intermediary_content.sub("", result) + candidate_lines.add(cleanup) + + target_files = search_files_recursively( + directory, targeted_filetypes_multiline_classB + ) + + for filepath in target_files: + with open(filepath) as _file: + print_verbose("processing %s for Java-style multi-line strings" % filepath) + filecontent = _file.read() + multiline_results = regex_java_style_multiline.findall(filecontent) + if len(multiline_results) > 0: + print_verbose("results: %d" % len(multiline_results)) + for result in multiline_results: + cleanup = regex_java_intermediary_content.sub("", result) + candidate_lines.add(cleanup) + + return candidate_lines + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("dictionary_output_file", help="output file", type=str) + parser.add_argument("--verbose", action="store_true", help="verbose stdout output") + + args = parser.parse_args() + verbose = args.verbose + + collected_candidate_lines = recursive_dictionary_extraction(TARGET_DIR) + sorted_candidate_lines = sorted(collected_candidate_lines) + result_lines = [] + for candidate_line in sorted_candidate_lines: + if ignore_general(candidate_line): + continue + result_lines.append( + encode_strings_for_dictionary(detect_and_convert_hex(candidate_line)) + ) + + print_verbose("counter_hex_content: %d" % counter_hex_content) + print_verbose( + "counter_wycheproof_hex_reconstruction: %d" + % counter_wycheproof_hex_reconstruction + ) + print_verbose("overall deduplicated entries: %d" % len(sorted_candidate_lines)) + + with open(args.dictionary_output_file, "w") as _file: + for result_line in result_lines: + _file.write("%s\n" % result_line) diff --git a/crypto/fuzzer/extract_fuzzer_dictionary.sh b/crypto/fuzzer/extract_fuzzer_dictionary.sh index 780fb5303..da928a998 100755 --- a/crypto/fuzzer/extract_fuzzer_dictionary.sh +++ b/crypto/fuzzer/extract_fuzzer_dictionary.sh @@ -2,19 +2,28 @@ # usage: script.sh target-dictionary-filename -# this script searches for interesting strings in the source code and converts +# This script searches for interesting strings in the source code and converts # them into a standard fuzzer dictionary file. +# +# Note that this script is phased out in favor of the more sophisticated +# extract_fuzzer_dictionary.py program +# TODO known issues: the end result has some duplicates in it TARGET_DIR=../tests OUTPUT_FILE=${1:-fuzzer_crypto_tests_strings_dictionary1.txt} -# empty file +multiline_string_search() { + # TODO the `find` regex behavior is Linux-specific + find $TARGET_DIR -type f -regextype posix-extended -regex '.*\.(c|h|py|json|java|js)' | xargs cat | perl -p0e 's/"\s*\n\s*\"//smg' +} + +# ensure empty file echo -n "" > $OUTPUT_FILE # strip multiline strings and extract them # exclude some hex strings, but allow hex strings with mixed capitalization (Ethereum, rskip60) -find $TARGET_DIR -type f | xargs cat | perl -p0e 's/"\s*\n\s*\"//smg' | grep -P -o "\"[\w ]+\"" | grep -v -P "\"(([0-9a-f][0-9a-f])+|([0-9A-F][0-9A-F])+)\"" | sort | uniq | while read -r line ; do +multiline_string_search | grep -P -o "\"[\w ]+\"" | grep -v -P "\"(([0-9a-f][0-9a-f])+|([0-9A-F][0-9A-F])+)\"" | sort | uniq | while read -r line ; do echo "$line" >> $OUTPUT_FILE done @@ -26,7 +35,7 @@ done # find each file, cat it, concatenate multiline strings, look for hex strings in quotes # note that this returns multiple megabyte of result strings due to the large amount # of test cases in the wycheproof project subfolder -find $TARGET_DIR -type f | xargs cat | perl -p0e 's/"\s*\n\s*\"//smg' | grep -P -o "\"([0-9a-fA-F][0-9a-fA-F])+\"" | grep -P -o "([0-9a-fA-F][0-9a-fA-F])+" | sort | uniq | while read -r line ; do +multiline_string_search | grep -P -o "\"([0-9a-fA-F][0-9a-fA-F])+\"" | grep -P -o "([0-9a-fA-F][0-9a-fA-F])+" | sort | uniq | while read -r line ; do # turn ascii hex strings AA into \xaa for the fuzzer format and add quotes # extra backslash escape due to the bash nesting escaped_hex=`echo $line | sed -e 's/../\\\\x&/g'` @@ -35,7 +44,6 @@ done # search and reassemble BIP39 test seeds that span multiple lines # find each file, cat it, concatenate multiline strings, look for BIP39 seed combinations with reasonable length -find $TARGET_DIR -type f | xargs cat | perl -p0e 's/"\s*\n\s*\"//smg' | grep -Po "(\w{3,10} ){11,23}(\w{3,10})" | sort | uniq | while read -r line ; do +multiline_string_search | grep -Po "(\w{3,10} ){11,23}(\w{3,10})" | sort | uniq | while read -r line ; do echo "\"$line\"" >> $OUTPUT_FILE done - diff --git a/crypto/fuzzer/fuzzer.c b/crypto/fuzzer/fuzzer.c index 2da5d3027..f16f4b313 100644 --- a/crypto/fuzzer/fuzzer.c +++ b/crypto/fuzzer/fuzzer.c @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2021 Christian Reitter + * Copyright (c) 2020-2022 Christian Reitter * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the "Software"), @@ -43,6 +43,7 @@ #include "ed25519-donna/ed25519-donna.h" #include "ed25519-donna/ed25519-keccak.h" #include "ed25519-donna/ed25519.h" +#include "hasher.h" #include "hmac_drbg.h" #include "memzero.h" #include "monero/monero.h" @@ -60,11 +61,9 @@ #include "shamir.h" #include "slip39.h" #include "slip39_wordlist.h" -#include "hasher.h" -#include "nist256p1.h" -#include "rand.h" -#include "secp256k1.h" - +#include "zkp_bip340.h" +#include "zkp_context.h" +#include "zkp_ecdsa.h" /* fuzzer input data handling */ const uint8_t *fuzzer_ptr; @@ -126,7 +125,7 @@ int fuzz_bn_format(void) { } else { return 0; } - // TODO fuzzer idea: allow prefix=NULL + // TODO idea: : allow prefix == NULL uint8_t suffixlen = 0; if (fuzzer_length < 1) { @@ -143,7 +142,7 @@ int fuzz_bn_format(void) { } else { return 0; } - // TODO fuzzer idea: allow suffix=NULL + // TODO idea: allow suffix == NULL uint32_t decimals = 0; int32_t exponent = 0; bool trailing = false; @@ -397,7 +396,7 @@ int fuzz_nem_get_address(void) { #if defined(__has_feature) #if __has_feature(memory_sanitizer) - // TODO check `address` for memory info leakage + // TODO idea: check `address` for memory info leakage #endif #endif @@ -504,20 +503,22 @@ int fuzz_shamir_interpolate(void) { return 0; } -int fuzz_ecdsa_sign_digest(void) { +int fuzz_ecdsa_sign_digest_functions(void) { + // bug result reference: https://github.com/trezor/trezor-firmware/pull/1697 + uint8_t curve_decider = 0; - uint8_t sig[64] = {0}; uint8_t priv_key[32] = {0}; uint8_t digest[32] = {0}; - if (fuzzer_length < 1 + sizeof(sig) + sizeof(priv_key) + sizeof(digest)) { + uint8_t sig1[64] = {0}; + uint8_t sig2[64] = {0}; + uint8_t pby1, pby2 = 0; + if (fuzzer_length < 1 + sizeof(priv_key) + sizeof(digest)) { return 0; } const ecdsa_curve *curve; - uint8_t pby = 0; memcpy(&curve_decider, fuzzer_input(1), 1); - memcpy(&sig, fuzzer_input(sizeof(sig)), sizeof(sig)); memcpy(&priv_key, fuzzer_input(sizeof(priv_key)), sizeof(priv_key)); memcpy(&digest, fuzzer_input(sizeof(digest)), sizeof(digest)); @@ -528,11 +529,29 @@ int fuzz_ecdsa_sign_digest(void) { curve = &nist256p1; } - // TODO optionally set a function for is_canonical() callback - int res = ecdsa_sign_digest(curve, priv_key, digest, sig, &pby, NULL); + int res = 0; + + // TODO idea: optionally set a function for is_canonical() callback + int res1 = ecdsa_sign_digest(curve, priv_key, digest, sig1, &pby1, NULL); + + // the zkp function variant is only defined for a specific curve + if (curve == &secp256k1) { + int res2 = + zkp_ecdsa_sign_digest(curve, priv_key, digest, sig2, &pby2, NULL); + if ((res1 == 0 && res2 != 0) || (res1 != 0 && res2 == 0)) { + // one variant succeeded where the other did not + crash(); + } + if (res1 == 0 && res2 == 0) { + if ((pby1 != pby2) || memcmp(&sig1, &sig2, sizeof(sig1)) != 0) { + // result values are different + crash(); + } + } + } // successful signing - if (res == 0) { + if (res1 == 0) { uint8_t pub_key[33] = {0}; res = ecdsa_get_public_key33(curve, priv_key, pub_key); if (res != 0) { @@ -540,7 +559,7 @@ int fuzz_ecdsa_sign_digest(void) { crash(); } - res = ecdsa_verify_digest(curve, pub_key, sig, digest); + res = ecdsa_verify_digest(curve, pub_key, sig1, digest); if (res != 0) { // verification did not succeed crash(); @@ -549,7 +568,7 @@ int fuzz_ecdsa_sign_digest(void) { return 0; } -int fuzz_ecdsa_verify_digest(void) { +int fuzz_ecdsa_verify_digest_functions(void) { uint8_t curve_decider = 0; uint8_t hash[32] = {0}; uint8_t sig[64] = {0}; @@ -572,23 +591,35 @@ int fuzz_ecdsa_verify_digest(void) { curve = &nist256p1; } - int res = ecdsa_verify_digest(curve, (const uint8_t *)&pub_key, - (const uint8_t *)&sig, (const uint8_t *)&hash); - - if (res == 0) { + int res1 = ecdsa_verify_digest(curve, (const uint8_t *)&pub_key, + (const uint8_t *)&sig, (const uint8_t *)&hash); + if (res1 == 0) { // See if the fuzzer ever manages to get find a correct verification // intentionally trigger a crash to make this case observable // TODO this is not an actual problem, remove in the future crash(); } + // the zkp_ecdsa* function only accepts the secp256k1 curve + if (curve == &secp256k1) { + int res2 = + zkp_ecdsa_verify_digest(curve, (const uint8_t *)&pub_key, + (const uint8_t *)&sig, (const uint8_t *)&hash); + + // the error code behavior is different between both functions, compare only + // verification state + if ((res1 == 0 && res2 != 0) || (res1 != 0 && res2 == 0)) { + // results differ, this is a problem + crash(); + } + } + return 0; } int fuzz_word_index(void) { #define MAX_WORD_LENGTH 12 - // TODO exact match? if (fuzzer_length < MAX_WORD_LENGTH) { return 0; } @@ -683,6 +714,27 @@ int fuzz_mnemonic_to_seed(void) { return 0; } +int fuzz_ethereum_address_checksum(void) { + uint8_t addr[20] = {0}; + char address[41] = {0}; + uint64_t chain_id = 0; + bool rskip60 = false; + + if (fuzzer_length < sizeof(addr) + sizeof(address) + sizeof(chain_id) + 1) { + return 0; + } + + memcpy(addr, fuzzer_input(sizeof(addr)), sizeof(addr)); + memcpy(address, fuzzer_input(sizeof(address)), sizeof(address)); + memcpy(&chain_id, fuzzer_input(sizeof(chain_id)), sizeof(chain_id)); + // usually dependent on chain_id, but determined separately here + rskip60 = (*fuzzer_input(1)) & 0x1; + + ethereum_address_checksum(addr, address, rskip60, chain_id); + + return 0; +} + int fuzz_aes(void) { if (fuzzer_length < 1 + 16 + 16 + 32) { return 0; @@ -776,7 +828,7 @@ int fuzz_b58gph_encode_decode(void) { return 0; } - // TODO switch to malloc()'ed buffers for better out of bounds access + // TODO idea: switch to malloc()'ed buffers for better out of bounds access // detection? uint8_t encode_in_buffer[BASE58_GPH_MAX_INPUT_LEN] = {0}; @@ -819,7 +871,7 @@ int fuzz_schnorr_verify_digest(void) { return 0; } - // TODO optionally try nist256p1 ? + // TODO idea: optionally try nist256p1 ? const ecdsa_curve *curve = &secp256k1; uint8_t digest[SHA256_DIGEST_LENGTH] = {0}; uint8_t pub_key[SCHNORR_VERIFY_PUBKEY_DATA_LENGTH] = {0}; @@ -887,9 +939,6 @@ int fuzz_schnorr_sign_digest(void) { return 0; } -// TODO zkp_bip340_sign, see test_check.c -// TODO zkp_bip340_verify, see test_check.c - int fuzz_chacha_drbg(void) { #define CHACHA_DRBG_ENTROPY_LENGTH 32 #define CHACHA_DRBG_RESEED_LENGTH 32 @@ -907,7 +956,7 @@ int fuzz_chacha_drbg(void) { uint8_t result[CHACHA_DRBG_RESULT_LENGTH] = {0}; CHACHA_DRBG_CTX ctx; - // TODO improvement idea: switch to variable input sizes + // TODO idea: switch to variable input sizes memcpy(&entropy, fuzzer_input(CHACHA_DRBG_ENTROPY_LENGTH), CHACHA_DRBG_ENTROPY_LENGTH); memcpy(&reseed, fuzzer_input(CHACHA_DRBG_RESEED_LENGTH), @@ -947,24 +996,215 @@ int fuzz_ed25519_sign_verify(void) { // verify message, we expect this to work ret = ed25519_sign_open(message, sizeof(message), public_key, signature); - // TODO are there other error values? - if (ret == -1) { + if (ret != 0) { + // verification did not succeed + crash(); + } + + return 0; +} + +int fuzz_zkp_bip340_sign_digest(void) { + // int res = 0; + uint8_t priv_key[32] = {0}; + uint8_t aux_input[32] = {0}; + uint8_t digest[32] = {0}; + uint8_t pub_key[32] = {0}; + uint8_t sig[64] = {0}; + + if (fuzzer_length < + sizeof(priv_key) + sizeof(aux_input) + sizeof(digest) + sizeof(sig)) { + return 0; + } + memcpy(priv_key, fuzzer_input(sizeof(priv_key)), sizeof(priv_key)); + memcpy(aux_input, fuzzer_input(sizeof(aux_input)), sizeof(aux_input)); + memcpy(digest, fuzzer_input(sizeof(digest)), sizeof(digest)); + memcpy(sig, fuzzer_input(sizeof(sig)), sizeof(sig)); + + zkp_bip340_get_public_key(priv_key, pub_key); + zkp_bip340_sign_digest(priv_key, digest, sig, aux_input); + // TODO idea: test sign result? + + return 0; +} + +int fuzz_zkp_bip340_verify_digest(void) { + int res = 0; + uint8_t pub_key[32] = {0}; + uint8_t digest[32] = {0}; + uint8_t sig[64] = {0}; + + if (fuzzer_length < sizeof(digest) + sizeof(pub_key) + sizeof(sig)) { + return 0; + } + memcpy(pub_key, fuzzer_input(sizeof(pub_key)), sizeof(pub_key)); + memcpy(digest, fuzzer_input(sizeof(digest)), sizeof(digest)); + memcpy(sig, fuzzer_input(sizeof(sig)), sizeof(sig)); + + res = zkp_bip340_verify_digest(pub_key, sig, digest); + + // res == 0 is valid, but crash to make successful passes visible + if (res == 0) { + crash(); + } + + return 0; +} + +int fuzz_zkp_bip340_tweak_keys(void) { + int res = 0; + uint8_t internal_priv[32] = {0}; + uint8_t root_hash[32] = {0}; + uint8_t internal_pub[32] = {0}; + uint8_t result[32] = {0}; + + if (fuzzer_length < + sizeof(internal_priv) + sizeof(root_hash) + sizeof(internal_pub)) { + return 0; + } + memcpy(internal_priv, fuzzer_input(sizeof(internal_priv)), + sizeof(internal_priv)); + memcpy(root_hash, fuzzer_input(sizeof(root_hash)), sizeof(root_hash)); + memcpy(internal_pub, fuzzer_input(sizeof(internal_pub)), + sizeof(internal_pub)); + + res = zkp_bip340_tweak_private_key(internal_priv, root_hash, result); + res = zkp_bip340_tweak_public_key(internal_pub, root_hash, result); + (void)res; + + return 0; +} + +int fuzz_ecdsa_get_public_key_functions(void) { + uint8_t privkey[32] = {0}; + uint8_t pubkey33_1[33] = {0}; + uint8_t pubkey33_2[33] = {0}; + uint8_t pubkey65_1[65] = {0}; + uint8_t pubkey65_2[65] = {0}; + + // note: the zkp_ecdsa_* variants require this specific curve + const ecdsa_curve *curve = &secp256k1; + + if (fuzzer_length < sizeof(privkey)) { + return 0; + } + memcpy(privkey, fuzzer_input(sizeof(privkey)), sizeof(privkey)); + + int res_33_1 = ecdsa_get_public_key33(curve, privkey, pubkey33_1); + int res_33_2 = zkp_ecdsa_get_public_key33(curve, privkey, pubkey33_2); + int res_65_1 = ecdsa_get_public_key65(curve, privkey, pubkey65_1); + int res_65_2 = zkp_ecdsa_get_public_key65(curve, privkey, pubkey65_2); + + // the function pairs have different return error codes for the same input + // so only fail if the one succeeds where the other does not + if ((res_33_1 == 0 && res_33_2 != 0) || (res_33_1 != 0 && res_33_2 == 0)) { + // function result mismatch + crash(); + } + if ((res_65_1 == 0 && res_65_2 != 0) || (res_65_1 != 0 && res_65_2 == 0)) { + // function result mismatch + crash(); + } + + if (res_33_1 == 0 && res_33_2 == 0 && + memcmp(&pubkey33_1, &pubkey33_2, sizeof(pubkey33_1)) != 0) { + // function result data mismatch + crash(); + } + + if (res_65_1 == 0 && res_65_2 == 0 && + memcmp(&pubkey65_1, &pubkey65_2, sizeof(pubkey65_1)) != 0) { + // function result data mismatch crash(); } return 0; } -// TODO more XMR functions -// extern void xmr_hash_to_ec(ge25519 *P, const void *data, size_t length); +int fuzz_ecdsa_recover_pub_from_sig_functions(void) { + uint8_t digest[32] = {0}; + uint8_t sig[64] = {0}; + const ecdsa_curve *curve = &secp256k1; + uint8_t recid = 0; + uint8_t pubkey1[65] = {0}; + uint8_t pubkey2[65] = {0}; + + if (fuzzer_length < sizeof(digest) + sizeof(sig) + sizeof(recid)) { + return 0; + } + memcpy(digest, fuzzer_input(sizeof(digest)), sizeof(digest)); + memcpy(sig, fuzzer_input(sizeof(sig)), sizeof(sig)); + memcpy(&recid, fuzzer_input(sizeof(recid)), sizeof(recid)); + // conform to parameter requirements + recid = recid & 0x03; + + int res1 = zkp_ecdsa_recover_pub_from_sig(curve, pubkey1, sig, digest, recid); + int res2 = ecdsa_recover_pub_from_sig(curve, pubkey2, sig, digest, recid); -// this function directly calls -// hasher_Raw(HASHER_SHA3K, data, length, hash) -// is this interesting at all? -// extern void xmr_fast_hash(uint8_t *hash, const void *data, size_t length); + uint8_t zero_pubkey[65] = {0}; + zero_pubkey[0] = 0x04; -// TODO target idea: re-create openssl_check() from test_openssl.c -// to do differential fuzzing against OpenSSL functions + if ((res1 == 0 && res2 != 0) || (res1 != 0 && res2 == 0)) { + // result mismatch + // bug result reference: https://github.com/trezor/trezor-firmware/pull/2050 + crash(); + } + + if (res1 == 0 && res2 == 0 && + memcmp(&pubkey1, &pubkey2, sizeof(pubkey1)) != 0) { + // pubkey result mismatch + crash(); + } + + return 0; +} + +int fuzz_ecdsa_sig_from_der(void) { + // bug result reference: https://github.com/trezor/trezor-firmware/pull/2058 + uint8_t der[72] = {0}; + uint8_t out[72] = {0}; + + if (fuzzer_length < sizeof(der)) { + return 0; + } + memcpy(der, fuzzer_input(sizeof(der)), sizeof(der)); + // null-terminate + der[sizeof(der) - 1] = 0; + size_t der_len = strlen((const char *)der); + + // TODO idea: use different fuzzer-controlled der_len such as 1 to 73 + int ret = ecdsa_sig_from_der(der, der_len, out); + (void)ret; + // TODO idea: check if back conversion works + + return 0; +} + +int fuzz_ecdsa_sig_to_der(void) { + uint8_t sig[64] = {0}; + uint8_t der[72] = {0}; + + if (fuzzer_length < sizeof(sig)) { + return 0; + } + memcpy(sig, fuzzer_input(sizeof(sig)), sizeof(sig)); + + int ret = ecdsa_sig_to_der((const uint8_t *)&sig, der); + (void)ret; + // TODO idea: check if back conversion works + + return 0; +} + +void zkp_initialize_context_or_crash(void) { + // The current context usage has persistent side effects + // TODO switch to frequent re-initialization where necessary + if (!zkp_context_is_initialized()) { + if (zkp_context_init() != 0) { + crash(); + } + } +} #define META_HEADER_SIZE 3 @@ -977,12 +1217,13 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { fuzzer_reset_state(); + // this controls up to 256 different test cases uint8_t target_decision = data[0]; - // TODO use once necessary - // uint8_t subdecision = data[1]; + // data[1] is reserved for explicit sub decisions + // uint8_t target_sub_decision = data[1]; - // note: data[2] is reserved for future use + // data[2] is reserved for future use // assign the fuzzer payload data for the target functions fuzzer_ptr = data + META_HEADER_SIZE; @@ -1042,8 +1283,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { break; case 14: #ifdef FUZZ_ALLOW_SLOW + zkp_initialize_context_or_crash(); // slow through expensive bignum operations - fuzz_ecdsa_verify_digest(); + fuzz_ecdsa_verify_digest_functions(); #endif break; case 15: @@ -1074,8 +1316,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { break; case 23: #ifdef FUZZ_ALLOW_SLOW + zkp_initialize_context_or_crash(); // slow through expensive bignum operations - fuzz_ecdsa_sign_digest(); + fuzz_ecdsa_sign_digest_functions(); #endif break; case 24: @@ -1087,7 +1330,35 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { case 26: fuzz_mnemonic_to_seed(); break; - + case 30: + fuzz_ethereum_address_checksum(); + break; + case 41: + zkp_initialize_context_or_crash(); + fuzz_zkp_bip340_sign_digest(); + break; + case 42: + zkp_initialize_context_or_crash(); + fuzz_zkp_bip340_verify_digest(); + break; + case 43: + zkp_initialize_context_or_crash(); + fuzz_zkp_bip340_tweak_keys(); + break; + case 50: + zkp_initialize_context_or_crash(); + fuzz_ecdsa_get_public_key_functions(); + break; + case 51: + zkp_initialize_context_or_crash(); + fuzz_ecdsa_recover_pub_from_sig_functions(); + break; + case 52: + fuzz_ecdsa_sig_from_der(); + break; + case 53: + fuzz_ecdsa_sig_to_der(); + break; default: // do nothing break;