feat(crypto): improve trezor-crypto fuzzer, add new dictionary extraction program

Introduce fuzzing harnesses for zkp* functions and adapt some differential fuzzing
Additional documentation and minor cleanup
Add temporary workaround for clang-14 and more explicit Makefile behavior
pull/2204/head
Christian Reitter 2 years ago committed by Andrew Kozlik
parent 47a05720aa
commit cf3c57d0ae

@ -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

@ -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

@ -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
# (?<!\+ ) and similar patterns are too static
regex_java_style_multiline = re.compile(
r"(?:\".+\"\s*\n\s*\+\s*)+(?:\".+\")", re.MULTILINE
)
regex_java_intermediary_content = re.compile(r"\"\s*\n\s*\+\s*\"", re.MULTILINE)
regex_text_newline = re.compile(r"\\n")
# primitive regex that catches most filenames in the data set
regex_filename_heuristic = re.compile(r"\.[a-zA-Z]+")
counter_hex_content = 0
counter_wycheproof_hex_reconstruction = 0
# TODO add '"curve"' to capture algorithm names?
allowlist_keywords_json = (
'"uncompressed"',
'"wx"',
'"wy"',
'"msg"',
'"sig"',
'"key"',
'"iv"',
'"ct"',
'"aad"',
'"tag"',
'"public"',
'"private"',
'"shared"',
'"padding"',
'"x"',
'"d"',
)
# TODO the "keyPem" entry is only a workaround for an encoding issue
ignore_keywords_java = (
"println(",
"Exception(",
'"keyPem"',
)
ignore_keywords_c = ("printf(",)
def ignore_single_line_json(data):
"""return True if the input should be ignored"""
# ignore everything that is not matched by the allowlist
for keyword in allowlist_keywords_json:
if data.find(keyword) > -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)

@ -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

@ -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;

Loading…
Cancel
Save