Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
url = https://github.com/digitalbitbox/yajl.git
[submodule "secp256k1"]
path = src/secp256k1
url = https://github.com/digitalbitbox/secp256k1.git
url = https://github.com/digitalbitbox/secp256k1_patch.git
Copy link
Member Author

@douglasbakkum douglasbakkum May 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cleaned up the commit history in the secp repository. This entailed a force push, so I archived the existing repository, in order for older releases of the firmware to still be able to be built, and created a new 'patch' repository to link to.

[submodule "hidapi"]
path = tests/hidapi
url = https://github.com/digitalbitbox/hidapi.git
2 changes: 1 addition & 1 deletion src/ecc_bitcoin.c
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ int libsecp256k1_ecc_ecdh(const uint8_t *pair_pubkey, const uint8_t *rand_privke
}

if (!secp256k1_ecdh(libsecp256k1_ctx, ecdh_secret_compressed, &pubkey_secp,
rand_privkey)) {
rand_privkey, NULL, NULL)) {
return 1;
}

Expand Down
228 changes: 154 additions & 74 deletions src/ecdh.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,44 +41,91 @@
#include "yajl/src/api/yajl_tree.h"

#define SIZE_BYTE 8

#define SIZE_EC_POINT_COMPRESSED 33
#define SIZE_EC_POINT_COMPRESSED_HEX 66
#define SIZE_EC_PRIVATE_KEY 32

#define SIZE_SHA256_HEX 64
#define CHALLENGE_BIT_POSITION_START 1
#define CHALLENGE_BIT_POSITION_MAX 4
#define CHALLENGE_BYTE_POSITION_START 0
#define CHALLENGE_MIN_SETS_BIT_VALUE (((CHALLENGE_MIN_BLINK_SETS - 1) % CHALLENGE_BIT_POSITION_MAX) + CHALLENGE_BIT_POSITION_START)
#define CHALLENGE_MIN_SETS_BYTE_VALUE ((CHALLENGE_MIN_BLINK_SETS - 1) / CHALLENGE_BIT_POSITION_MAX)


/**
* Holds the private and public elliptic-curve keypair generated for the DH-key exchange.
*/
typedef struct TFA_EC_Keypair {
typedef struct {
uint8_t private_key[SIZE_EC_PRIVATE_KEY];
uint8_t public_key[SIZE_EC_POINT_COMPRESSED];
} TFA_EC_Keypair;
} _keypair_t;

// Stores the SHA-256 hash of the ecdh public key from the mobile app.
__extension__ static uint8_t TFA_IN_HASH_PUB[] = {[0 ... SHA256_DIGEST_LENGTH - 1] = 0x00};
__extension__ static uint8_t TFA_ZEROS[] = {[0 ... SHA256_DIGEST_LENGTH - 1] = 0x00};
__extension__ static uint8_t _input_hash_pubkey[] = {[0 ... SHA256_DIGEST_LENGTH - 1] = 0x00};
__extension__ static uint8_t _zero_32[] = {[0 ... SHA256_DIGEST_LENGTH - 1] = 0x00};

// Stores the ecdh keypair used in creating the shared secret for the mobile
// app communication.
__extension__ static TFA_EC_Keypair tfa_keypair = {
__extension__ static _keypair_t _keypair = {
{[0 ... SIZE_EC_PRIVATE_KEY - 1] = 0x00},
{[0 ... SIZE_EC_POINT_COMPRESSED - 1] = 0x00}
};

// The position of the yet-to-be-verified byte.
static uint8_t TFA_VERIFY_BYTEPOS = 0;
static uint8_t _challenge_byte_position = CHALLENGE_BYTE_POSITION_START;

// The position of the current 2 bit inside the yet-to-be-verified byte.
// Valid values are 1 to 4.
static uint8_t TFA_VERIFY_BITPOS = 1;
static uint8_t _challenge_bit_position = CHALLENGE_BIT_POSITION_START;

// Enforced order:
// 1 hash_pubkey (can be called anytime to start over)
// 2 pubkey (cannot be called twice)
// 3 challenge (can be called repeatedly)
// - An abort command requires starting over
typedef enum {
CMD_HASH_PUBKEY,
CMD_PUBKEY,
CMD_CHALLENGE,
CMD_ABORTED,
} _cmd_order_t;
static uint8_t _last_command = CMD_ABORTED;

// The cached shared secret, set after a successful pubkey command.
// It is erased:
// - Immediately after written to eeprom
// - After an abort command
// - On a command error
static uint8_t _shared_secret[SIZE_ECDH_SHARED_SECRET] = {0};
#ifdef TESTING
static uint8_t _test_shared_secret[SIZE_ECDH_SHARED_SECRET] = {0};
uint8_t *test_shared_secret_report(void)
{
return _test_shared_secret;
}
#endif


/**
* Clear static variables
*/
static void _clear_static_variables(void)
{
#ifdef TESTING
utils_zero(_test_shared_secret, SIZE_ECDH_SHARED_SECRET);
#endif
utils_zero(_shared_secret, SIZE_ECDH_SHARED_SECRET);
utils_zero(_input_hash_pubkey, SHA256_DIGEST_LENGTH);
utils_zero(&_keypair, sizeof(_keypair_t));
utils_clear_buffers();
}


/**
* Generates a new EC key pair on the SECP256k1 curve and stores it in the address given as a
* parameter.
*/
static int ecdh_generate_key(TFA_EC_Keypair *keypair)
static int _generate_key(_keypair_t *keypair)
{
do {
if (random_bytes(keypair->private_key, sizeof(keypair->private_key), 0) == DBB_ERROR) {
Expand All @@ -90,46 +137,52 @@ static int ecdh_generate_key(TFA_EC_Keypair *keypair)
return DBB_OK;
}


/**
* Processes the { "ecdh" : { "hash_pubkey" : "..." } } command.
* The command triggers the (re-)generation of a new ECDH keypair,
* which requires a long-touch.
* If an error happens, the function prepares a response.
*/
static void ecdh_hash_pubkey_command(const char *pair_hash_pubkey)
static void _hash_pubkey_command(const char *pair_hash_pubkey)
{
if (strlens(pair_hash_pubkey) != SIZE_SHA256_HEX) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_KEY_HASH_ECDH_LEN);
return;
goto cleanup;
}

int status = touch_button_press(TOUCH_LONG_PAIR);
if (status != DBB_TOUCHED) {
utils_zero(TFA_IN_HASH_PUB, SHA256_DIGEST_LENGTH);
commander_fill_report(cmd_str(CMD_ecdh), NULL, status);
return;
goto cleanup;
}

TFA_VERIFY_BITPOS = 1;
TFA_VERIFY_BYTEPOS = 0;
int ret = ecdh_generate_key(&tfa_keypair);
_challenge_bit_position = CHALLENGE_BIT_POSITION_START;
_challenge_byte_position = CHALLENGE_BYTE_POSITION_START;
int ret = _generate_key(&_keypair);
if (ret != DBB_OK) {
utils_zero(TFA_IN_HASH_PUB, SHA256_DIGEST_LENGTH);
commander_fill_report(cmd_str(CMD_ecdh), NULL, ret);
return;
goto cleanup;
}

char msg[256];
uint8_t hash_pubkey[SHA256_DIGEST_LENGTH];
sha256_Raw(tfa_keypair.public_key, sizeof(tfa_keypair.public_key), hash_pubkey);
sha256_Raw(_keypair.public_key, sizeof(_keypair.public_key), hash_pubkey);
snprintf(msg, sizeof(msg), "{\"%s\":\"%s\"}",
cmd_str(CMD_hash_pubkey), utils_uint8_to_hex(hash_pubkey, sizeof(hash_pubkey)));

memcpy(TFA_IN_HASH_PUB, utils_hex_to_uint8(pair_hash_pubkey), SHA256_DIGEST_LENGTH);
_last_command = CMD_HASH_PUBKEY;
memcpy(_input_hash_pubkey, utils_hex_to_uint8(pair_hash_pubkey), SHA256_DIGEST_LENGTH);
commander_clear_report();
commander_fill_report(cmd_str(CMD_ecdh), msg, DBB_JSON_ARRAY);
return;

cleanup:
_last_command = CMD_ABORTED;
_clear_static_variables();
}


/**
* Process the { "ecdh" : { "pubkey" : "..." } } command.
* First, we check whether the provided public key is the one to which the pairing partner
Expand All @@ -138,8 +191,14 @@ static void ecdh_hash_pubkey_command(const char *pair_hash_pubkey)
* Lastly, the hash of the pairing partner's public key and keypair for generating the shared
* secret is reset.
*/
static void ecdh_pubkey_command(const char *pair_pubkey)
static void _pubkey_command(const char *pair_pubkey)
{
if (_last_command != CMD_HASH_PUBKEY) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_IO_CMD_ORDER);
goto cleanup;
}
_last_command = CMD_PUBKEY;

// 66 bytes because it's the hex representation of a compressed EC public key.
if (strlens(pair_pubkey) != SIZE_EC_POINT_COMPRESSED_HEX) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_KEY_ECDH_LEN);
Expand All @@ -150,13 +209,13 @@ static void ecdh_pubkey_command(const char *pair_pubkey)
memcpy(pair_pubkey_bytes, utils_hex_to_uint8(pair_pubkey), SIZE_EC_POINT_COMPRESSED);

// Point-at-infinity not allowed
if (MEMEQ(TFA_ZEROS, pair_pubkey_bytes + 1, SHA256_DIGEST_LENGTH)) {
if (MEMEQ(_zero_32, pair_pubkey_bytes + 1, SHA256_DIGEST_LENGTH)) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_TFA_HASH_MATCH);
goto cleanup;
}

// Check if hashed pubkey was provided
if (MEMEQ(TFA_ZEROS, TFA_IN_HASH_PUB, SHA256_DIGEST_LENGTH)) {
if (MEMEQ(_zero_32, _input_hash_pubkey, SHA256_DIGEST_LENGTH)) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_TFA_HASH_MATCH);
goto cleanup;
}
Expand All @@ -165,36 +224,31 @@ static void ecdh_pubkey_command(const char *pair_pubkey)
sha256_Raw(pair_pubkey_bytes, SIZE_EC_POINT_COMPRESSED, h);

// Check if the pubkey matches the provided hashed pubkey
if (!MEMEQ(h, TFA_IN_HASH_PUB, SHA256_DIGEST_LENGTH)) {
if (!MEMEQ(h, _input_hash_pubkey, SHA256_DIGEST_LENGTH)) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_TFA_HASH_MATCH);
goto cleanup;
}

uint8_t ecdh_shared_secret[SIZE_ECDH_SHARED_SECRET];
if (bitcoin_ecc.ecc_ecdh(pair_pubkey_bytes, tfa_keypair.private_key, ecdh_shared_secret,
if (bitcoin_ecc.ecc_ecdh(pair_pubkey_bytes, _keypair.private_key, _shared_secret,
ECC_SECP256k1)) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_KEY_ECDH);
goto cleanup;
}

uint8_t ret = memory_write_tfa_shared_secret(ecdh_shared_secret);
if (ret != DBB_OK) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, ret);
goto cleanup;
}
#ifdef TESTING
memcpy(_test_shared_secret, _shared_secret, SIZE_ECDH_SHARED_SECRET);
#endif

char msg[256];
snprintf(msg, sizeof(msg), "{\"%s\":\"%s\"}",
cmd_str(CMD_pubkey), utils_uint8_to_hex(tfa_keypair.public_key,
sizeof(tfa_keypair.public_key)));
cmd_str(CMD_pubkey), utils_uint8_to_hex(_keypair.public_key,
sizeof(_keypair.public_key)));

commander_fill_report(cmd_str(CMD_ecdh), msg, DBB_JSON_OBJECT);
return;

cleanup:
utils_zero(TFA_IN_HASH_PUB, SHA256_DIGEST_LENGTH);
utils_zero(&tfa_keypair, sizeof(TFA_EC_Keypair));
utils_clear_buffers();

_last_command = CMD_ABORTED;
_clear_static_variables();
}

/**
Expand All @@ -203,44 +257,67 @@ static void ecdh_pubkey_command(const char *pair_pubkey)
* 2 bits at the current position of the shared secret.
* The position is advanced afterwards.
*/
static void ecdh_challenge_command(void)
static void _challenge_command(void)
{
uint8_t *shared_secret = memory_report_aeskey(TFA_SHARED_SECRET);
uint8_t encryption_and_authentication_key[SHA512_DIGEST_LENGTH];
uint8_t encryption_and_authentication_challenge[SHA256_DIGEST_LENGTH];

sha512_Raw(shared_secret, SIZE_ECDH_SHARED_SECRET, encryption_and_authentication_key);
sha256_Raw(encryption_and_authentication_key, SHA512_DIGEST_LENGTH,
encryption_and_authentication_challenge);

uint8_t two_bit = (encryption_and_authentication_challenge[TFA_VERIFY_BYTEPOS] >>
(SIZE_BYTE - 2 *
TFA_VERIFY_BITPOS)) & 3;

TFA_VERIFY_BITPOS = (TFA_VERIFY_BITPOS + 1) % 5;
if (TFA_VERIFY_BITPOS == 0) {
TFA_VERIFY_BYTEPOS = (TFA_VERIFY_BYTEPOS + 1) % SIZE_ECDH_SHARED_SECRET;
TFA_VERIFY_BITPOS = 1;
if (_last_command != CMD_PUBKEY && _last_command != CMD_CHALLENGE) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_IO_CMD_ORDER);
goto cleanup;
}
led_2FA_pairing_code(two_bit + 1);

utils_zero(encryption_and_authentication_key, SHA512_DIGEST_LENGTH);
utils_zero(encryption_and_authentication_challenge, SHA256_DIGEST_LENGTH);
_last_command = CMD_CHALLENGE;

// Save the shared secret to eeprom only after the challenge command has been
// repeated CHALLENGE_MIN_BLINK_SETS times. Then erase the cached shared secret, now
// that it is 'accepted'. The challenge is a hash of a secret and not a secret itself.
static uint8_t challenge[SHA256_DIGEST_LENGTH] = {0};
if (_challenge_bit_position == CHALLENGE_BIT_POSITION_START &&
_challenge_byte_position == CHALLENGE_BYTE_POSITION_START) {
uint8_t encryption_and_authentication_key[SHA512_DIGEST_LENGTH] = {0};
sha512_Raw(_shared_secret, SIZE_ECDH_SHARED_SECRET, encryption_and_authentication_key);
sha256_Raw(encryption_and_authentication_key, SHA512_DIGEST_LENGTH, challenge);
utils_zero(encryption_and_authentication_key, SHA512_DIGEST_LENGTH);
}
if (_challenge_bit_position == CHALLENGE_MIN_SETS_BIT_VALUE &&
_challenge_byte_position == CHALLENGE_MIN_SETS_BYTE_VALUE) {
int ret = memory_write_tfa_shared_secret(_shared_secret);
_clear_static_variables();
if (ret != DBB_OK) {
commander_fill_report(cmd_str(CMD_ecdh), NULL, ret);
goto cleanup;
}
}

uint8_t two_bit = (challenge[_challenge_byte_position] >>
(SIZE_BYTE - 2 * _challenge_bit_position)) & 3;

_challenge_bit_position = (_challenge_bit_position + 1) % (CHALLENGE_BIT_POSITION_MAX +
1);
if (_challenge_bit_position == 0) {
_challenge_byte_position = (_challenge_byte_position + 1) % SIZE_ECDH_SHARED_SECRET;
_challenge_bit_position = CHALLENGE_BIT_POSITION_START;
}

led_2FA_pairing_code(two_bit + 1);

commander_fill_report(cmd_str(CMD_ecdh), attr_str(ATTR_success), DBB_JSON_STRING);
return;

cleanup:
_last_command = CMD_ABORTED;
_clear_static_variables();
}


/**
* Aborts the pairing process and thereby resets the positions for blinking.
*/
static void ecdh_abort_command(void)
static void _abort(void)
{
utils_zero(TFA_IN_HASH_PUB, SHA256_DIGEST_LENGTH);
utils_zero(&tfa_keypair, sizeof(TFA_EC_Keypair));
utils_clear_buffers();
TFA_VERIFY_BITPOS = 1;
TFA_VERIFY_BYTEPOS = 0;
commander_fill_report(cmd_str(CMD_ecdh), cmd_str(ATTR_aborted), DBB_JSON_STRING);
_clear_static_variables();
_last_command = CMD_ABORTED;
_challenge_bit_position = CHALLENGE_BIT_POSITION_START;
_challenge_byte_position = CHALLENGE_BYTE_POSITION_START;
commander_fill_report(cmd_str(CMD_ecdh), attr_str(ATTR_aborted), DBB_JSON_STRING);
}

/**
Expand Down Expand Up @@ -273,17 +350,20 @@ void ecdh_dispatch_command(yajl_val json_node)
yajl_val pair_abort = yajl_tree_get(data, pair_abort_path, yajl_t_true);

if (strlens(pair_hash_pubkey)) {
ecdh_hash_pubkey_command(pair_hash_pubkey);
_hash_pubkey_command(pair_hash_pubkey);
return;
} else if (strlens(pair_pubkey)) {
ecdh_pubkey_command(pair_pubkey);
_pubkey_command(pair_pubkey);
return;
} else if (YAJL_IS_TRUE(pair_challenge)) {
ecdh_challenge_command();
_challenge_command();
return;
} else if (YAJL_IS_TRUE(pair_abort)) {
ecdh_abort_command();
} else {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_IO_INVALID_CMD);
_abort();
return;
}
} else {
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_IO_INVALID_CMD);
}

_abort();
commander_fill_report(cmd_str(CMD_ecdh), NULL, DBB_ERR_IO_INVALID_CMD);
}
Loading