mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-03-12 06:06:07 +00:00
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
This commit is contained in:
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
|
||||
|
||||
|
389
crypto/fuzzer/extract_fuzzer_dictionary.py
Executable file
389
crypto/fuzzer/extract_fuzzer_dictionary.py
Executable file
@ -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;
|
||||
}
|
||||
|
||||
// TODO more XMR functions
|
||||
// extern void xmr_hash_to_ec(ge25519 *P, const void *data, size_t length);
|
||||
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};
|
||||
|
||||
// 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);
|
||||
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));
|
||||
|
||||
// TODO target idea: re-create openssl_check() from test_openssl.c
|
||||
// to do differential fuzzing against OpenSSL functions
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
uint8_t zero_pubkey[65] = {0};
|
||||
zero_pubkey[0] = 0x04;
|
||||
|
||||
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…
Reference in New Issue
Block a user