Skip to content

Commit

Permalink
Merge branch 'quickfixes'
Browse files Browse the repository at this point in the history
Last minute corrections:
- Always skip authentication over CCID (by compilation flag), and correct
	tests to honor it.
- Allow to build without CCID support.
- Update docs.
  • Loading branch information
szszszsz committed Apr 24, 2023
2 parents 977bef8 + c25a055 commit d30abc1
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 117 deletions.
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ project(nitrokey_hotp_verification VERSION 1.4)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-guess-branch-probability -Wdate-time -frandom-seed=device.c -O0")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -gno-record-gcc-switches -DNDEBUG")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -gno-record-gcc-switches ")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNDEBUG ") #comment out to show debug output
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdebug-prefix-map=${CMAKE_CURRENT_BINARY_DIR}=heads")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS}")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -D_DEBUG")
SET(CMAKE_EXE_LINKER_FLAGS "-O0")
SET(CMAKE_EXE_LINKER_FLAGS "-O1 -fsanitize=address")

set(GIT_VERSION_PLACEHOLDER "no-git-version")

Expand Down
52 changes: 29 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# Nitrokey HOTP verification tool
# Nitrokey HOTP Verification Tool

Nitrokey [HOTP](https://tools.ietf.org/html/rfc4226) verification tool is a command line application, which communicates with a custom Nitrokey (over HIDAPI/libusb) to configure HOTP secrets and to verify HOTP codes. The Nitrokey being used needs to support HOTP verification, which is - at the time of writing - supported by a custom firmware for Nitrokey Pro.
Nitrokey [HOTP](https://tools.ietf.org/html/rfc4226) Verification Tool is a command line application, which communicates with a HOTP USB Security Dongle (over HIDAPI/libusb or CCID) to configure HOTP secrets and to verify HOTP codes. The Dongle being used needs to support HOTP verification, which is - at the time of writing - supported by Nitrokey Pro, Nitrokey Storage, Nitrokey 3 and Librem Key.

This solution is meant to allow users to verify the authenticity of his computer. During the boot process of the computer, the user will be asked to insert his Nitrokey device, which verifies the HOTP code generated and sent by the computer. The Nitrokey indicates the verification result by LED: success = green LED blinking; failure = red LED blinking quicker.
This solution is meant to allow one to verify the authenticity of his computer. During the boot process, the user will be asked to insert his HOTP USB Security Dongle device, which would then compare the on-device generated HOTP code and the one sent by the computer under verification. Afterward the Nitrokey indicates the verification result by LED animation:
- OTP codes are equal - success => green LED blinking;
- OTP codes are different - failure => red LED blinking quicker.


## Requirements
This tool uses [HIDAPI](https://github.com/Nitrokey/hidapi) library to communicate with the Nitrokey device. It is a light wrapper over the `libusb` API and requires `usb-1.0` library at the link time.

This tool also reads from `/dev/urandom` to generate session secret for device-authorization purposes.
For HID use this tool also reads from `/dev/urandom` to generate a session secret for the device-authorization purposes used in libnitrokey.

The Nitrokey device needs to support HOTP verification.
The USB HOTP Security Dongle device needs to support HOTP verification.

The CCID interface is implemented to support Nitrokey 3, which uses [Secrets App](https://github.com/Nitrokey/trussed-secrets-app) for its OTP handling.

## Compilation

Expand Down Expand Up @@ -79,7 +83,7 @@ To set a new HOTP secret to be verified on the device please run:
```
where:
- `BASE32 HOTP SECRET` is a new base32 HOTP secret, with up to 160 bits of length;
- `ADMIN PIN` is a current Admin PIN of the device;
- `ADMIN PIN` is a current Admin PIN of the device. Nitrokey 3 allows to skip providing it by accepting empty string as an argument: `""`;
- `COUNTER` is an optional argument holding an initial value for the HOTP counter to be set on the device.

#### Verifying HOTP code
Expand All @@ -90,24 +94,26 @@ To verify the HOTP code please run `check` command as in:
where:
`HOTP CODE` is a 6-digits HOTP code to verify

In case where the code is verified on the device, the green LED will blink 5 times. Otherwise the red LED will blink 5 times, twice as fast than green.
In case where the code is verified on the device, the green LED will blink 5 times. Otherwise, the red LED will blink 5 times, twice as fast as the green.

Verification of 8-digits codes is available as an option through build configuration in [settings.h](settings.h). Responsible flag is configured on device during the HOTP secret setup phase and cannot be changed (until another secret rewrite) in the current implementation.

Solution contains a mean to avoid desynchronization between the host's and device's counters.
Solution contains means to avoid desynchronization between the host's and device's counters.
Device calculates up to 9 values ahead of its current counter to find the matching code (in total it calculates HOTP code for 10 subsequent counter positions). In case:
- no code would match - the on-device counter will not be changed;
- code would match, but with some counter's offset (up to 9) - the on-device counter will be set to matched code-generated HOTP counter and incremented by 1;
- code would match, and the code matches counter without offset - the counter will be incremented by 1.

Device will stop verifying the HOTP codes in case, when the difference between the host and on-device counters will be greater or equal to 10.

This allows to boot the system without USB Security Dongle 9 times, until it would lose synchronization and would need to be set up again.

#### Identifying the device
To show information about the connected device please use:
```bash
$ ./nitrokey_hotp_verification info
# which results should be similar to:
HOTP code verification application via Nitrokey Pro
HOTP code verification application, version 1.4
Connected device status:
Card serial: 0x5F11
Firmware: v0.9
Expand All @@ -119,7 +125,7 @@ $ ./nitrokey_hotp_verification id
```

#### AES key regeneration
Tool supports AES key regeneration call, which should be called after each GnuPG factory-reset operation. Example call:
Tool supports AES key regeneration call, which should be called after each GnuPG factory-reset operation for Nitrokey Pro, Librem Key and Nitrokey Storage devices. Example call:

```bash
./nitrokey_hotp_verification regenerate 12345678
Expand All @@ -140,7 +146,7 @@ Tool supports AES key regeneration call, which should be called after each GnuPG

#### Help screen
```bash
HOTP code verification application via Nitrokey Pro, version 1.0-alpha
HOTP code verification application, version 1.4
Available commands:
./nitrokey_hotp_verification id
./nitrokey_hotp_verification info
Expand All @@ -154,18 +160,18 @@ Available commands:
#### Exit codes
In case the tool would encounter any critical issues, it will print error message and return to the OS with a proper exit code value. Meaning of the exit values could be checked with the following table:

Error code name | Exit code | Meaning
--- | :---: | ---
EXIT_NO_ERROR | 0 | Operation was completed successfully or HOTP code was confirmed to be valid
EXIT_CONNECTION_ERROR | 1 | Could not connect to the Nitrokey Pro device
EXIT_WRONG_PIN | 2 | Could not authorize the user with the user given PIN
EXIT_OTHER_ERROR | 3 | Unknown error
EXIT_INVALID_HOTP_CODE | 4 | Entered HOTP code was calculated to be invalid
EXIT_UNKNOWN_COMMAND | 5 | Device does not support HOTP verification command in this firmware
EXIT_SLOT_NOT_PROGRAMMED | 6 | On-device slot was not programmed with HOTP secret yet
EXIT_BAD_FORMAT | 7 | Either entered HOTP code for validation or base32 secret to set was in improper format (too long or consisting of invalid characters)
EXIT_CONNECTION_LOST | 8 | Connection to the device was lost during the process
EXIT_INVALID_PARAMS | 100 | Application could not parse command line arguments
| Error code name | Exit code | Meaning |
|--------------------------|:---------:|---------------------------------------------------------------------------------------------------------------------------------------|
| EXIT_NO_ERROR | 0 | Operation was completed successfully or HOTP code was confirmed to be valid |
| EXIT_CONNECTION_ERROR | 1 | Could not connect to the Nitrokey Pro device |
| EXIT_WRONG_PIN | 2 | Could not authorize the user with the user given PIN |
| EXIT_OTHER_ERROR | 3 | Unknown error |
| EXIT_INVALID_HOTP_CODE | 4 | Entered HOTP code was calculated to be invalid |
| EXIT_UNKNOWN_COMMAND | 5 | Device does not support HOTP verification command in this firmware |
| EXIT_SLOT_NOT_PROGRAMMED | 6 | On-device slot was not programmed with HOTP secret yet |
| EXIT_BAD_FORMAT | 7 | Either entered HOTP code for validation or base32 secret to set was in improper format (too long or consisting of invalid characters) |
| EXIT_CONNECTION_LOST | 8 | Connection to the device was lost during the process |
| EXIT_INVALID_PARAMS | 100 | Application could not parse command line arguments |

## Tests
Solution was tested against 160-bits test vectors available at [RFC_HOTP-test-vectors.txt](RFC_HOTP-test-vectors.txt).
Expand Down
37 changes: 19 additions & 18 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,6 @@ project('hotp-verification', 'c',
version : '1.4.0',
)
lusb = dependency('libusb-1.0')

src = [
'src/crc32.c',
'src/device.c',
'src/operations.c',
'src/dev_commands.c',
'src/base32.c',
'src/random_data.c',
'src/min.c',
'src/version.c',
'src/return_codes.c',
'src/main.c',
'src/tlv.c',
'src/ccid.c',
'src/operations_ccid.c',
'hidapi/libusb/hid.c'
]

version_array = meson.project_version().split('.')
version_major = version_array[0].to_int()
version_minor = version_array[1].to_int()
Expand All @@ -39,6 +21,25 @@ version_cc = vcs_tag(
fallback : 'v@0@'.format(meson.project_version()),
)


src = [
'src/crc32.c',
'src/device.c',
'src/operations.c',
'src/dev_commands.c',
'src/base32.c',
'src/random_data.c',
'src/min.c',
version_cc,
'src/return_codes.c',
'src/main.c',
'src/tlv.c',
'src/ccid.c',
'src/operations_ccid.c',
'hidapi/libusb/hid.c'
]


common_flags = [
'-DNK_REMOVE_PTHREAD',
'-fdebug-prefix-map=$PWD=heads',
Expand Down
79 changes: 50 additions & 29 deletions src/ccid.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>

static const int READ_ENDPOINT = 0x81;

Expand All @@ -39,7 +40,7 @@ static const int WRITE_ENDPOINT = 0x01;
static const int TIMEOUT = 1000;


uint32_t icc_compose(uint8_t *buf, uint32_t buffer_length, uint8_t msg_type, int32_t data_len, uint8_t slot, uint8_t seq, uint16_t param, uint8_t *data) {
uint32_t icc_compose(uint8_t *buf, uint32_t buffer_length, uint8_t msg_type, size_t data_len, uint8_t slot, uint8_t seq, uint16_t param, uint8_t *data) {
static int _seq = 0;
if (seq == 0) {
seq = _seq++;
Expand All @@ -48,10 +49,12 @@ uint32_t icc_compose(uint8_t *buf, uint32_t buffer_length, uint8_t msg_type, int
size_t i = 0;
buf[i++] = msg_type;

buf[i++] = data_len << 0;
buf[i++] = data_len << 8;
buf[i++] = data_len << 16;
buf[i++] = data_len << 24;
rassert(data_len < INT32_MAX);
int32_t _data_len = (int32_t) data_len;
buf[i++] = _data_len << 0;
buf[i++] = _data_len << 8;
buf[i++] = _data_len << 16;
buf[i++] = _data_len << 24;

buf[i++] = slot;
buf[i++] = seq;
Expand Down Expand Up @@ -99,6 +102,8 @@ IccResult parse_icc_result(uint8_t *buf, size_t buf_len) {
// .buffer = buf,
// .buffer_len = buf_len
};
// Make sure the response do not contain overread attempts
rassert(i.data_len < buf_len - 10);
return i;
}

Expand Down Expand Up @@ -154,52 +159,74 @@ libusb_device_handle *get_device(libusb_context *ctx, const struct VidPid pPid[]
}


int ccid_process_single(libusb_device_handle *handle, uint8_t *buf, uint32_t buf_length, const uint8_t *d,
const uint32_t length, IccResult *result) {
int ccid_process_single(libusb_device_handle *handle, uint8_t *receiving_buffer, uint32_t receiving_buffer_length, const uint8_t *sending_buffer,
const uint32_t sending_buffer_length, IccResult *result) {
int actual_length = 0, r;

r = ccid_send(handle, &actual_length, d, length);
r = ccid_send(handle, &actual_length, sending_buffer, sending_buffer_length);
if (r != 0) {
return r;
}

int prev_status = 0;
while (true) {
r = ccid_receive(handle, &actual_length, buf, buf_length);
r = ccid_receive(handle, &actual_length, receiving_buffer, receiving_buffer_length);
if (r != 0) {
return r;
}

IccResult iccResult = parse_icc_result(buf, buf_length);
IccResult iccResult = parse_icc_result(receiving_buffer, receiving_buffer_length);
LOG("status %d, chain %d\n", iccResult.status, iccResult.chain);
if (iccResult.data_len > 0) {
print_buffer(iccResult.data, iccResult.data_len, " returned data");
LOG("Status code: %s\n", ccid_error_message(iccResult.data_status_code));
}
if (iccResult.data[0] == 0x61) {// 0x61 status code means data remaining, make another receive
if (iccResult.data[0] == DATA_REMAINING_STATUS_CODE) {
// 0x61 status code means data remaining, make another receive call

uint8_t buf_sr[128];
uint8_t buf_sr[SMALL_CCID_BUFFER_SIZE] = {};
uint32_t send_rem_length = iso7816_compose(buf_sr, sizeof buf_sr,
Ins_GetResponse, 0, 0, 0, 0xFF, NULL, 0);
uint8_t buf_sr_2[128];
uint8_t buf_sr_2[SMALL_CCID_BUFFER_SIZE] = {};
uint32_t send_rem_icc_len = icc_compose(buf_sr_2, sizeof buf_sr_2,
0x6F, send_rem_length,
0, 0, 0, buf_sr);
int actual_length_sr = 0;
uint8_t buf_sr_recv[MAX_CCID_BUFFER_SIZE];
// FIXME handle returned r errors
r = ccid_send(handle, &actual_length_sr, buf_sr_2, send_rem_icc_len);
r = ccid_receive(handle, &actual_length_sr, buf_sr_recv, sizeof buf_sr_recv);
iccResult = parse_icc_result(buf_sr_recv, sizeof buf_sr_recv);
if (r != 0) {
return r;
}

memset(receiving_buffer, 0, receiving_buffer_length);
r = ccid_receive(handle, &actual_length_sr, receiving_buffer, receiving_buffer_length);
if (r != 0) {
return r;
}

iccResult = parse_icc_result(receiving_buffer, receiving_buffer_length);
LOG("status %d, chain %d\n", iccResult.status, iccResult.chain);
if (iccResult.data_len > 0) {
print_buffer(iccResult.data, iccResult.data_len, " returned data");
LOG("Status code: %s\n", ccid_error_message(iccResult.data_status_code));
}
}
if (iccResult.status == 0x80) {
printf("Please touch the USB security key if it blinks\n");
if (iccResult.status == AWAITING_FOR_TOUCH_STATUS_CODE) {
if (prev_status != iccResult.status) {
printf("Please touch the USB security key if it blinks ");
fflush(stdout);
prev_status = iccResult.status;
} else {
printf(".");
fflush(stdout);
}
continue;
} else {
if (prev_status == AWAITING_FOR_TOUCH_STATUS_CODE) {
printf(". touch received\n");
fflush(stdout);
}
}
prev_status = iccResult.status;
if (iccResult.chain == 0 || iccResult.chain == 2) {
if (result != NULL) {
memmove(result, &iccResult, sizeof iccResult);
Expand Down Expand Up @@ -344,33 +371,26 @@ int ccid_init(libusb_device_handle *handle) {
0x00,
};

// unsigned char cmd_reset[] = {
// 0x6f,0x04,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x04,0xde,0xad,
// };

const unsigned char *data_to_send[] = {
cmd_select,
cmd_poweron,
cmd_poweroff,
cmd_info,
// cmd_reset
};

const unsigned int data_to_send_size[] = {
sizeof(cmd_select),
sizeof(cmd_poweron),
sizeof(cmd_poweroff),
sizeof(cmd_info),
// sizeof(cmd_reset)
};

// FIXME set the proper CCID buffer length (270 was not enough)
unsigned char buf[MAX_CCID_BUFFER_SIZE] = {};
ccid_process(handle, buf, sizeof buf, data_to_send, LEN_ARR(data_to_send), data_to_send_size, true, NULL);
return 0;
}

int icc_pack_tlvs_for_sending(uint8_t *buf, size_t buflen, TLV *tlvs, int tlvs_count, int ins) {
uint32_t icc_pack_tlvs_for_sending(uint8_t *buf, size_t buflen, TLV *tlvs, int tlvs_count, int ins) {
uint8_t data_tlvs[MAX_CCID_BUFFER_SIZE] = {};
int tlvs_actual_length = process_all(data_tlvs, tlvs, tlvs_count);

Expand All @@ -389,8 +409,9 @@ int icc_pack_tlvs_for_sending(uint8_t *buf, size_t buflen, TLV *tlvs, int tlvs_c
return icc_actual_length;
}

int ccid_receive(libusb_device_handle *device, int *actual_length, unsigned char *returned_data, int buffer_length) {
int r = libusb_bulk_transfer(device, READ_ENDPOINT, returned_data, buffer_length, actual_length, TIMEOUT);
int ccid_receive(libusb_device_handle *device, int *actual_length, unsigned char *returned_data, size_t buffer_length) {
int32_t _buffer_length = MIN(buffer_length, INT32_MAX);
int r = libusb_bulk_transfer(device, READ_ENDPOINT, returned_data, _buffer_length, actual_length, TIMEOUT);
if (r < 0) {
LOG("Error reading data: %s\n", libusb_strerror(r));
return 1;
Expand Down
Loading

0 comments on commit d30abc1

Please sign in to comment.