crypto: improve fuzz testing code, harnesses, documentation and scripts

pull/1744/head
Christian Reitter 4 years ago committed by Andrew Kozlik
parent 4f7c6b3586
commit 78f879aaf1

@ -1,4 +1,4 @@
# trezor-crypto fuzzing
# Fuzz Testing trezor-crypto
Selected functions can be fuzzed via specific libFuzzer harnesses for increased test coverage and issue detection.
Note: the following commands are relative to the trezor-crypto main directory.
@ -19,15 +19,17 @@ Recommended: ASAN / UBSAN / MSAN flags for error detection can be specified via
Examples:
* `SANFLAGS="-fsanitize=address,undefined"`
* `SANFLAGS="-fsanitize=memory"`
* `SANFLAGS="-fsanitize=memory -fsanitize-memory-track-origins"`
### Optimizations
Override `OPTFLAGS` to test the library at different optimization levels or simplify the debugging of detected issues.
Example:
Examples:
* `OPTFLAGS="-O0 -ggdb3"`
* `OPTFLAGS="-O3 -march=native"`
## Operation
@ -46,18 +48,22 @@ mkdir fuzzer/fuzzer_corpus
./fuzzer/fuzzer -max_len=2048 -use_value_profile=1 -jobs=16 -timeout=1 -reload=5 -print_pcs=1 -print_funcs=42 fuzzer/fuzzer_corpus
```
Hint: for more permanent setups, consider invoking the fuzzer from outside of the source directory to avoid cluttering it with logfiles and crash inputs.
Hint: for more permanent setups, consider invoking the fuzzer from outside of the source directory to avoid cluttering it with logfiles and crash inputs. Similarly, it is recommended to store the fuzzer corpus in another location.
## Automated fuzzer dictionary generation
## Automated Fuzzer Dictionary Generation
[Dictionaries](https://llvm.org/docs/LibFuzzer.html#dictionaries) are a useful mechanism to augment the capabilities of the fuzzer. Specify them via the `-dict=` flag.
### Collect interesting strings from the unit tests
``` bash
grep -r -P -o -h "\"\w+\"" tests | sort | uniq > fuzzer_crypto_tests_strings_dictionary1.txt
### Collect Interesting Strings From Unit Tests
```bash
cd fuzzer
./extract_fuzzer_dictionary.sh fuzzer_crypto_tests_strings_dictionary1.txt
```
The resulting file can be used as a fuzzer dictionary.
## Evaluate source coverage
## Evaluate Source Coverage
1. build the fuzzer binary with `CFLAGS="-fprofile-instr-generate -fcoverage-mapping"`
1. run with suitable `-runs=` or `-max_total_time=` limits

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# usage: script.sh target-dictionary-filename
# this script searches for interesting strings in the source code and converts
# them into a standard fuzzer dictionary file.
TARGETFILE=${1:-fuzzer_crypto_tests_strings_dictionary1.txt}
# collect strings with normal words from the tests
grep -r -P -o -h "\"[\w ]+\"" ../tests | sort | uniq > $TARGETFILE
# collect BIP39 and SLIP39 words
grep -r -P -o -h "\"\w+\"" ../slip39_wordlist.h ../bip39_english.h | sort | uniq >> $TARGETFILE
# hex string to quoted escaped hex conversion
# TODO add an inverted output variant with swapped endian order?
grep -r -P -o -h "([0-9a-fA-F][0-9a-fA-F])+" ../tests | sort | uniq | \
while read -r line ; do
# double escape since it is going to be used in bash
escaped_hex=`echo $line | sed -e 's/../\\\\x&/g'`
echo "\"$escaped_hex\"" >> $TARGETFILE
done
# search and reassemble BIP39 seed mnemonics that span multiple lines from the tests
# valid words are 3 to 10 characters long and there are 12, 18 or 24 words in a valid mnemonic
grep -r -P -o -h "\"(\w{3,10} ?)+\",?" ../tests | grep -vP "[0-9A-Z]" | tr '"\n' ' ' | \
sed 's/ / /g' | sed 's/ / /g'| grep -Po "(\w{3,10} ){11,23}(\w{3,10})" | sort | uniq | \
while read -r line ; do
echo "\"$line\"" >> $TARGETFILE
done

@ -20,16 +20,25 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// necessary for the target functions
#include "aes/aes.h"
#include "bignum.h"
#include "ecdsa.h"
#include "hasher.h"
#include "nist256p1.h"
#include "rand.h"
#include "secp256k1.h"
#include "ed25519-donna/ed25519-donna.h"
#include "ed25519-donna/ed25519.h"
#include "nem.h"
#include "shamir.h"
/* fuzzer input data handling */
const uint8_t *fuzzer_ptr;
@ -58,13 +67,14 @@ void fuzzer_reset_state(void) {
extern size_t bn_format(const bignum256 *amnt, const char *prefix,
const char *suffix, unsigned int decimals, int exponent,
bool trailing, char *out, size_t outlen);
int fuzz_bn_format() {
int fuzz_bn_format(void) {
bignum256 target_bignum;
// we need some amount of data, bail if the input is too short
if (fuzzer_length < sizeof(target_bignum)) {
return 0;
}
char buf[512];
char buf[512] = {0};
int r;
// mutate the struct contents
@ -77,6 +87,8 @@ int fuzz_bn_format() {
}
memcpy(&prefixlen, fuzzer_input(1), 1);
char prefix[prefixlen];
memset(&prefix, 0, prefixlen);
if (prefixlen > 0 && prefixlen <= 128 && prefixlen <= fuzzer_length) {
memcpy(&prefix, fuzzer_input(prefixlen), prefixlen);
// force null termination
@ -92,6 +104,8 @@ int fuzz_bn_format() {
}
memcpy(&suffixlen, fuzzer_input(1), 1);
char suffix[suffixlen];
memset(&suffix, 0, suffixlen);
if (suffixlen > 0 && suffixlen <= 128 && suffixlen <= fuzzer_length) {
memcpy(&suffix, fuzzer_input(suffixlen), suffixlen);
// force null termination
@ -125,7 +139,7 @@ extern uint8_t *base32_decode(const char *in, size_t inlen, uint8_t *out,
// arbitrarily chosen maximum size
#define BASE32_DECODE_MAX_INPUT_LEN 512
int fuzz_base32_decode() {
int fuzz_base32_decode(void) {
if (fuzzer_length < 2 || fuzzer_length > BASE32_DECODE_MAX_INPUT_LEN) {
return 0;
}
@ -153,7 +167,7 @@ extern char *base32_encode(const uint8_t *in, size_t inlen, char *out,
// arbitrarily chosen maximum size
#define BASE32_ENCODE_MAX_INPUT_LEN 512
int fuzz_base32_encode() {
int fuzz_base32_encode(void) {
if (fuzzer_length > BASE32_ENCODE_MAX_INPUT_LEN) {
return 0;
}
@ -178,7 +192,7 @@ extern int base58_encode_check(const uint8_t *data, int datalen,
// internal limit is 128, try some extra bytes
#define BASE58_ENCODE_MAX_INPUT_LEN 140
int fuzz_base58_encode_check() {
int fuzz_base58_encode_check(void) {
if (fuzzer_length > BASE58_ENCODE_MAX_INPUT_LEN) {
return 0;
}
@ -207,7 +221,7 @@ extern int base58_decode_check(const char *str, HasherType hasher_type,
// internal limit is 128, try some extra bytes
#define BASE58_DECODE_MAX_INPUT_LEN 140
int fuzz_base58_decode_check() {
int fuzz_base58_decode_check(void) {
if (fuzzer_length > BASE58_DECODE_MAX_INPUT_LEN) {
return 0;
}
@ -240,7 +254,7 @@ extern int xmr_base58_addr_decode_check(const char *addr, size_t sz,
// arbitrarily chosen maximum size
#define XMR_BASE58_ADDR_DECODE_MAX_INPUT_LEN 512
int fuzz_xmr_base58_addr_decode_check() {
int fuzz_xmr_base58_addr_decode_check(void) {
if (fuzzer_length > XMR_BASE58_ADDR_DECODE_MAX_INPUT_LEN) {
return 0;
}
@ -265,7 +279,7 @@ extern int xmr_base58_addr_encode_check(uint64_t tag, const uint8_t *data,
// arbitrarily chosen maximum size
#define XMR_BASE58_ADDR_ENCODE_MAX_INPUT_LEN 512
int fuzz_xmr_base58_addr_encode_check() {
int fuzz_xmr_base58_addr_encode_check(void) {
uint64_t tag_in;
size_t tag_size = sizeof(tag_in);
if (fuzzer_length < tag_size ||
@ -298,7 +312,7 @@ extern int xmr_read_varint(uint8_t *buff, size_t buff_size, uint64_t *val);
// arbitrarily chosen maximum size
#define XMR_SERIALIZE_VARINT_MAX_INPUT_LEN 128
int fuzz_xmr_serialize_varint() {
int fuzz_xmr_serialize_varint(void) {
uint64_t varint_in;
size_t varint_in_size = sizeof(varint_in);
if (fuzzer_length < varint_in_size ||
@ -333,7 +347,7 @@ extern bool nem_validate_address(const char *address, uint8_t network);
// arbitrarily chosen maximum size
#define NEM_VALIDATE_ADDRESS_MAX_INPUT_LEN 128
int fuzz_nem_validate_address() {
int fuzz_nem_validate_address(void) {
if (fuzzer_length < (1 + 1) ||
fuzzer_length > NEM_VALIDATE_ADDRESS_MAX_INPUT_LEN) {
return 0;
@ -358,6 +372,339 @@ int fuzz_nem_validate_address() {
return 0;
}
extern bool nem_get_address(const ed25519_public_key public_key,
uint8_t version, char *address);
int fuzz_nem_get_address(void) {
unsigned char ed25519_public_key[32] = {0};
uint32_t network = 0;
if (fuzzer_length != (sizeof(ed25519_public_key) + sizeof(network))) {
return 0;
}
char address[NEM_ADDRESS_SIZE + 1] = {0};
memcpy(ed25519_public_key, fuzzer_input(32), 32);
memcpy(&network, fuzzer_input(4), 4);
nem_get_address(ed25519_public_key, network, address);
// TODO check return address for memory info leakage?
return 0;
}
extern void xmr_get_subaddress_secret_key(bignum256modm r, uint32_t major,
uint32_t minor,
const bignum256modm m);
int fuzz_xmr_get_subaddress_secret_key(void) {
bignum256modm m = {0};
uint32_t major = 0;
uint32_t minor = 0;
if (fuzzer_length != (sizeof(bignum256modm) + 2 * sizeof(uint32_t))) {
return 0;
}
bignum256modm output = {0};
memcpy(m, fuzzer_input(sizeof(bignum256modm)), sizeof(bignum256modm));
memcpy(&major, fuzzer_input(sizeof(uint32_t)), sizeof(uint32_t));
memcpy(&minor, fuzzer_input(sizeof(uint32_t)), sizeof(uint32_t));
xmr_get_subaddress_secret_key(output, major, minor, m);
return 0;
}
extern void xmr_derive_private_key(bignum256modm s, const ge25519 *deriv,
uint32_t idx, const bignum256modm base);
int fuzz_xmr_derive_private_key(void) {
bignum256modm base = {0};
ge25519 deriv = {0};
uint32_t idx = 0;
if (fuzzer_length !=
(sizeof(bignum256modm) + sizeof(ge25519) + sizeof(uint32_t))) {
return 0;
}
memcpy(base, fuzzer_input(sizeof(bignum256modm)), sizeof(bignum256modm));
memcpy(&deriv, fuzzer_input(sizeof(ge25519)), sizeof(ge25519));
memcpy(&idx, fuzzer_input(sizeof(uint32_t)), sizeof(uint32_t));
bignum256modm output = {0};
xmr_derive_private_key(output, &deriv, idx, base);
return 0;
}
extern void xmr_derive_public_key(ge25519 *r, const ge25519 *deriv,
uint32_t idx, const ge25519 *base);
int fuzz_xmr_derive_public_key(void) {
ge25519 base = {0};
ge25519 deriv = {0};
uint32_t idx = 0;
if (fuzzer_length != (2 * sizeof(ge25519) + sizeof(uint32_t))) {
return 0;
}
memcpy(&base, fuzzer_input(sizeof(ge25519)), sizeof(ge25519));
memcpy(&deriv, fuzzer_input(sizeof(ge25519)), sizeof(ge25519));
memcpy(&idx, fuzzer_input(sizeof(uint32_t)), sizeof(uint32_t));
ge25519 output = {0};
xmr_derive_public_key(&output, &deriv, idx, &base);
return 0;
}
extern bool shamir_interpolate(uint8_t *result, uint8_t result_index,
const uint8_t *share_indices,
const uint8_t **share_values,
uint8_t share_count, size_t len);
#define SHAMIR_MAX_SHARE_COUNT 16
#define SHAMIR_MAX_DATA_LEN (SHAMIR_MAX_SHARE_COUNT * SHAMIR_MAX_LEN)
int fuzz_shamir_interpolate(void) {
if (fuzzer_length != (2 * sizeof(uint8_t) + SHAMIR_MAX_SHARE_COUNT +
SHAMIR_MAX_DATA_LEN + 2)) {
return 0;
}
uint8_t result[SHAMIR_MAX_LEN] = {0};
uint8_t result_index = 0;
uint8_t share_indices[SHAMIR_MAX_SHARE_COUNT] = {0};
uint8_t share_values_content[SHAMIR_MAX_SHARE_COUNT][SHAMIR_MAX_LEN] = {0};
const uint8_t *share_values[SHAMIR_MAX_SHARE_COUNT] = {0};
uint8_t share_count = 0;
size_t len = 0;
for (size_t i = 0; i < SHAMIR_MAX_SHARE_COUNT; i++) {
share_values[i] = share_values_content[i];
}
memcpy(&result_index, fuzzer_input(sizeof(uint8_t)), sizeof(uint8_t));
memcpy(&share_indices, fuzzer_input(SHAMIR_MAX_SHARE_COUNT),
SHAMIR_MAX_SHARE_COUNT);
memcpy(&share_values_content, fuzzer_input(SHAMIR_MAX_DATA_LEN),
SHAMIR_MAX_DATA_LEN);
memcpy(&share_count, fuzzer_input(sizeof(uint8_t)), sizeof(uint8_t));
// the target function checks for (len > SHAMIR_MAX_LEN),
// so we don't have to test the whole size_t length value
len = (fuzzer_ptr[0] << 8) + fuzzer_ptr[1];
fuzzer_input(2);
// mirror the checks in mod_trezorcrypto_shamir_interpolate()
if (share_count < 1 || share_count > SHAMIR_MAX_SHARE_COUNT) {
return 0;
}
shamir_interpolate(result, result_index, share_indices, share_values,
share_count, len);
return 0;
}
extern int ecdsa_verify_digest(const ecdsa_curve *curve, const uint8_t *pub_key,
const uint8_t *sig, const uint8_t *digest);
int fuzz_ecdsa_verify_digest(void) {
uint8_t curve_decider = 0;
uint8_t hash[32] = {0};
uint8_t sig[64] = {0};
uint8_t pub_key[65] = {0};
if (fuzzer_length < 1 + sizeof(hash) + sizeof(sig) + sizeof(pub_key)) {
return 0;
}
memcpy(&curve_decider, fuzzer_input(1), 1);
memcpy(&hash, fuzzer_input(sizeof(hash)), sizeof(hash));
memcpy(&sig, fuzzer_input(sizeof(sig)), sizeof(sig));
memcpy(&pub_key, fuzzer_input(sizeof(pub_key)), sizeof(pub_key));
const ecdsa_curve *curve;
// pick one of the standard curves
if ((curve_decider & 0x1) == 1) {
curve = &secp256k1;
} else {
curve = &nist256p1;
}
int res = ecdsa_verify_digest(curve, (const uint8_t *)&pub_key,
(const uint8_t *)&sig, (const uint8_t *)&hash);
// TODO check if the fuzzer ever manages to get the return value to 0
(void)res;
return 0;
}
extern bool word_index(uint16_t *index, const char *word, uint8_t word_length);
int fuzz_word_index(void) {
#define MAX_WORD_LENGTH 12
// TODO exact match?
if (fuzzer_length < MAX_WORD_LENGTH) {
return 0;
}
char word[MAX_WORD_LENGTH + 1] = {0};
memcpy(&word, fuzzer_ptr, MAX_WORD_LENGTH);
size_t word_length = strlen(word);
uint16_t index = 0;
word_index(&index, (const char *)&word, word_length);
return 0;
}
extern uint16_t slip39_word_completion_mask(uint16_t sequence);
int fuzz_slip39_word_completion_mask(void) {
if (fuzzer_length != 2) {
return 0;
}
uint16_t sequence = (fuzzer_ptr[0] << 8) + fuzzer_ptr[1];
fuzzer_input(2);
// TODO perform additional checks on the output?
slip39_word_completion_mask(sequence);
return 0;
}
extern int mnemonic_to_bits(const char *mnemonic, uint8_t *mnemonic_bits);
int fuzz_mnemonic_to_bits(void) {
// slightly longer than MAX_MNEMONIC_LEN from config.h
#define MAX_MNEMONIC_LENGTH 256
if (fuzzer_length < MAX_MNEMONIC_LENGTH) {
return 0;
}
char mnemonic[MAX_MNEMONIC_LENGTH + 1] = {0};
memcpy(&mnemonic, fuzzer_ptr, MAX_MNEMONIC_LENGTH);
uint8_t mnemonic_bits[32 + 1] = {0};
int number_of_bits = mnemonic_to_bits((const char *)&mnemonic, mnemonic_bits);
assert(0 <= number_of_bits && number_of_bits <= 264);
return 0;
}
int fuzz_aes(void) {
if (fuzzer_length < 1 + 16 + 16 + 32) {
return 0;
}
aes_encrypt_ctx ctxe;
aes_decrypt_ctx ctxd;
uint8_t ibuf[16] = {0};
uint8_t obuf[16] = {0};
uint8_t iv[16] = {0};
uint8_t cbuf[16] = {0};
const uint8_t *keylength_decider = fuzzer_input(1);
// note: the unit test uses the fixed 32 byte key
// 603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4
uint8_t keybuf[32] = {0};
memcpy(&keybuf, fuzzer_input(32), 32);
#ifdef AES_VAR
// try 128, 192, 256 bit key lengths
size_t keylength = 32;
switch (keylength_decider[0] & 0x3) {
case 0:
// invalid length
keylength = 1;
break;
case 1:
keylength = 16;
break;
case 2:
keylength = 24;
break;
case 3:
keylength = 32;
break;
}
if (aes_encrypt_key((const unsigned char *)&keybuf, keylength, &ctxe) ||
aes_decrypt_key((const unsigned char *)&keybuf, keylength, &ctxd)) {
// initialization problems, stop processing
// we expect this to happen with the invalid key length
return 0;
}
#else
// use a 256 bit key length
(void)keylength_decider;
aes_encrypt_key256((const unsigned char *)&keybuf, &ctxe);
aes_decrypt_key256((const unsigned char *)&keybuf, &ctxd);
#endif
memcpy(ibuf, fuzzer_input(16), 16);
memcpy(iv, fuzzer_input(16), 16);
aes_ecb_encrypt(ibuf, obuf, 16, &ctxe);
aes_ecb_decrypt(ibuf, obuf, 16, &ctxd);
aes_cbc_encrypt(ibuf, obuf, 16, iv, &ctxe);
aes_cbc_decrypt(ibuf, obuf, 16, iv, &ctxd);
aes_cfb_encrypt(ibuf, obuf, 16, iv, &ctxe);
aes_cfb_decrypt(ibuf, obuf, 16, iv, &ctxe);
aes_ofb_encrypt(ibuf, obuf, 16, iv, &ctxe);
aes_ofb_decrypt(ibuf, obuf, 16, iv, &ctxe);
aes_ctr_encrypt(ibuf, obuf, 16, cbuf, aes_ctr_cbuf_inc, &ctxe);
aes_ctr_decrypt(ibuf, obuf, 16, cbuf, aes_ctr_cbuf_inc, &ctxe);
return 0;
}
extern int base58gph_encode_check(const uint8_t *data, int datalen, char *str,
int strsize);
extern int base58gph_decode_check(const char *str, uint8_t *data, int datalen);
int fuzz_b58gph_encode_decode(void) {
// note: encode and decode have an internal limit of 128
#define BASE58_GPH_MAX_INPUT_LEN 140
if (fuzzer_length > BASE58_GPH_MAX_INPUT_LEN) {
return 0;
}
uint8_t encode_in_buffer[BASE58_GPH_MAX_INPUT_LEN] = {0};
// with null termination
char decode_in_buffer[BASE58_GPH_MAX_INPUT_LEN + 1] = {0};
char out_buffer[BASE58_GPH_MAX_INPUT_LEN] = {0};
size_t outlen = sizeof(out_buffer);
size_t raw_inlen = fuzzer_length;
memcpy(&encode_in_buffer, fuzzer_input(raw_inlen), raw_inlen);
memcpy(&decode_in_buffer, &encode_in_buffer, raw_inlen);
base58gph_encode_check(encode_in_buffer, raw_inlen, out_buffer, outlen);
base58gph_decode_check(decode_in_buffer, (uint8_t *)&out_buffer, outlen);
// TODO do logical encode<>decode comparison checks?
return 0;
}
#define META_HEADER_SIZE 3
// main fuzzer entry
@ -369,9 +716,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
fuzzer_reset_state();
uint8_t decision = data[0];
uint8_t target_decision = data[0];
// TODO use when necessary
// TODO use once necessary
// uint8_t subdecision = data[1];
// note: data[2] is reserved for future use
@ -380,7 +727,16 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
fuzzer_ptr = data + META_HEADER_SIZE;
fuzzer_length = size - META_HEADER_SIZE;
switch (decision) {
// if active: reject all other inputs that are not the selected target
// this is helpful for directing the fuzzing focus on a specific case
#ifdef FUZZER_EXCLUSIVE_TARGET
if (target_decision != FUZZER_EXCLUSIVE_TARGET) {
return 0;
}
#endif
// TODO reorder and regroup target functions
switch (target_decision) {
case 0:
fuzz_bn_format();
break;
@ -408,6 +764,45 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
case 8:
fuzz_nem_validate_address();
break;
case 9:
fuzz_nem_get_address();
break;
case 10:
fuzz_xmr_get_subaddress_secret_key();
break;
case 11:
fuzz_xmr_derive_private_key();
break;
case 12:
fuzz_xmr_derive_public_key();
break;
case 13:
fuzz_shamir_interpolate();
break;
case 14:
#ifdef FUZZ_ALLOW_SLOW
// slow through expensive bignum operations
fuzz_ecdsa_verify_digest();
#endif
break;
case 15:
fuzz_word_index();
break;
case 16:
fuzz_slip39_word_completion_mask();
break;
case 17:
fuzz_mnemonic_to_bits();
break;
case 18:
#ifdef FUZZ_ALLOW_SLOW
fuzz_aes();
#endif
break;
case 19:
fuzz_b58gph_encode_decode();
break;
default:
// do nothing
break;

Loading…
Cancel
Save