Skip to content

Commit

Permalink
Merge branch 'pro-bootloader'
Browse files Browse the repository at this point in the history
Add tests for the Nitrokey Pro bootloader support.
Allow to reconnect to the device during the test execution
(e.g. for a reinsertion).

Connected: Nitrokey/nitrokey-pro-firmware#69
  • Loading branch information
szszszsz committed Feb 26, 2020
2 parents 2c52393 + f37b771 commit 6100df4
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 119 deletions.
104 changes: 63 additions & 41 deletions unittest/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"""

import pytest
import os, sys

from misc import ffi, gs

Expand Down Expand Up @@ -82,47 +83,8 @@ def C(request=None):


def get_library(request, allow_offline=False):
fp = '../NK_C_API.h'

declarations = []
with open(fp, 'r') as f:
declarations = f.readlines()

cnt = 0
a = iter(declarations)
for declaration in a:
if declaration.strip().startswith('NK_C_API') \
or declaration.strip().startswith('struct'):
declaration = declaration.replace('NK_C_API', '').strip()
while ');' not in declaration and '};' not in declaration:
declaration += (next(a)).strip()+'\n'
ffi.cdef(declaration, override=True)
cnt += 1
print('Imported {} declarations'.format(cnt))

C = None
import os, sys
path_build = os.path.join("..", "build")
paths = [
os.environ.get('LIBNK_PATH', None),
os.path.join(path_build,"libnitrokey.so"),
os.path.join(path_build,"libnitrokey.dylib"),
os.path.join(path_build,"libnitrokey.dll"),
os.path.join(path_build,"nitrokey.dll"),
]
for p in paths:
if not p: continue
print("Trying " +p)
p = os.path.abspath(p)
if os.path.exists(p):
print("Found: "+p)
C = ffi.dlopen(p)
break
else:
print("File does not exist: " + p)
if not C:
print("No library file found")
sys.exit(1)
library_read_declarations()
C = library_open_lib()

C.NK_set_debug_level(int(os.environ.get('LIBNK_DEBUG', 2)))

Expand Down Expand Up @@ -155,3 +117,63 @@ def fin():

return AttrProxy(C, "libnitrokey C")


def library_open_lib():
C = None
path_build = os.path.join("..", "build")
paths = [
os.environ.get('LIBNK_PATH', None),
os.path.join(path_build, "libnitrokey.so"),
os.path.join(path_build, "libnitrokey.dylib"),
os.path.join(path_build, "libnitrokey.dll"),
os.path.join(path_build, "nitrokey.dll"),
]
for p in paths:
if not p: continue
print("Trying " + p)
p = os.path.abspath(p)
if os.path.exists(p):
print("Found: " + p)
C = ffi.dlopen(p)
break
else:
print("File does not exist: " + p)
if not C:
print("No library file found")
sys.exit(1)
return C


def library_read_declarations():
fp = '../NK_C_API.h'
declarations = []
with open(fp, 'r') as f:
declarations = f.readlines()
cnt = 0
a = iter(declarations)
for declaration in a:
if declaration.strip().startswith('NK_C_API') \
or declaration.strip().startswith('struct'):
declaration = declaration.replace('NK_C_API', '').strip()
while ');' not in declaration and '};' not in declaration:
declaration += (next(a)).strip() + '\n'
ffi.cdef(declaration, override=True)
cnt += 1
print('Imported {} declarations'.format(cnt))


def pytest_addoption(parser):
parser.addoption("--run-skipped", action="store_true",
help="run the tests skipped by default, e.g. adding side effects")

def pytest_runtest_setup(item):
if 'skip_by_default' in item.keywords and not item.config.getoption("--run-skipped"):
pytest.skip("need --run-skipped option to run this test")


def library_device_reconnect(C):
C.NK_logout()
C = library_open_lib()
C.NK_logout()
assert C.NK_login_auto() == 1, 'Device not found'
return C
14 changes: 7 additions & 7 deletions unittest/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@
SPDX-License-Identifier: LGPL-3.0
"""

from misc import to_hex

def bb(x):
return bytes(x, encoding='ascii')

from misc import to_hex, bb

RFC_SECRET_HR = '12345678901234567890'
RFC_SECRET = to_hex(RFC_SECRET_HR) # '31323334353637383930...'
Expand All @@ -39,6 +34,9 @@ class DefaultPasswords:
USER_TEMP = b'234234234'
UPDATE = b'12345678'
UPDATE_TEMP = b'123update123'
UPDATE_LONG = b'1234567890'*2
UPDATE_TOO_LONG = UPDATE_LONG + b'x'
UPDATE_TOO_SHORT = UPDATE_LONG[:7]


class DeviceErrorCode:
Expand All @@ -49,6 +47,7 @@ class DeviceErrorCode:
STATUS_NOT_AUTHORIZED = 5
STATUS_AES_DEC_FAILED = 0xa
STATUS_UNKNOWN_ERROR = 100
STATUS_DISCONNECTED = 255


class LibraryErrors:
Expand All @@ -59,4 +58,5 @@ class LibraryErrors:


HOTP_slot_count = 3
TOTP_slot_count = 15
TOTP_slot_count = 15
PWS_SLOT_COUNT = 16
51 changes: 51 additions & 0 deletions unittest/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from constants import DeviceErrorCode, PWS_SLOT_COUNT, DefaultPasswords
from misc import gs, bb


def helper_fill(str_to_fill, target_width):
assert target_width >= len(str_to_fill)
numbers = '1234567890' * 4
str_to_fill += numbers[:target_width - len(str_to_fill)]
assert len(str_to_fill) == target_width
return bb(str_to_fill)


def helper_PWS_get_pass(suffix):
return helper_fill('pass' + suffix, 20)


def helper_PWS_get_loginname(suffix):
return helper_fill('login' + suffix, 32)


def helper_PWS_get_slotname(suffix):
return helper_fill('slotname' + suffix, 11)


def helper_check_device_for_data(C):
assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK
assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK

for i in range(0, PWS_SLOT_COUNT):
iss = str(i)
assert gs(C.NK_get_password_safe_slot_name(i)) == helper_PWS_get_slotname(iss)
assert gs(C.NK_get_password_safe_slot_login(i)) == helper_PWS_get_loginname(iss)
assert gs(C.NK_get_password_safe_slot_password(i)) == helper_PWS_get_pass(iss)
return True


def helper_populate_device(C):
# FIXME use object with random data, and check against it
# FIXME generate OTP as well, and check codes against its secrets
assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK
res = C.NK_enable_password_safe(DefaultPasswords.USER)
if res != DeviceErrorCode.STATUS_OK:
assert C.NK_build_aes_key(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK
assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK

for i in range(0, PWS_SLOT_COUNT):
iss = str(i)
assert C.NK_write_password_safe_slot(i,
helper_PWS_get_slotname(iss), helper_PWS_get_loginname(iss),
helper_PWS_get_pass(iss)) == DeviceErrorCode.STATUS_OK
return True
4 changes: 4 additions & 0 deletions unittest/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ def is_long_OTP_secret_handled(C):

def has_binary_counter(C):
return (not is_storage(C)) or (is_storage(C) and get_devices_firmware_version(C) >= 54)


def bb(x):
return bytes(x, encoding='ascii')
4 changes: 2 additions & 2 deletions unittest/test_multiple.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
from tqdm import tqdm

from conftest import skip_if_device_version_lower_than
from constants import DefaultPasswords, DeviceErrorCode, bb
from misc import gs, wait, ffi
from constants import DefaultPasswords, DeviceErrorCode
from misc import gs, wait, ffi, bb

pprint = pprint.PrettyPrinter(indent=4).pprint

Expand Down
78 changes: 11 additions & 67 deletions unittest/test_pro.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
import pytest

from conftest import skip_if_device_version_lower_than
from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET, bb, bbRFC_SECRET, LibraryErrors, HOTP_slot_count, \
from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET, bbRFC_SECRET, LibraryErrors, HOTP_slot_count, \
TOTP_slot_count
from misc import ffi, gs, wait, cast_pointer_to_tuple, has_binary_counter
from helpers import helper_PWS_get_slotname, helper_PWS_get_loginname, helper_PWS_get_pass
from misc import ffi, gs, wait, cast_pointer_to_tuple, has_binary_counter, bb
from misc import is_storage

@pytest.mark.lock_device
Expand All @@ -50,70 +51,38 @@ def test_write_password_safe_slot(C):
@pytest.mark.PWS
@pytest.mark.slowtest
def test_write_all_password_safe_slots_and_read_10_times(C):
def fill(s, wid):
assert wid >= len(s)
numbers = '1234567890'*4
s += numbers[:wid-len(s)]
assert len(s) == wid
return bb(s)

def get_pass(suffix):
return fill('pass' + suffix, 20)

def get_loginname(suffix):
return fill('login' + suffix, 32)

def get_slotname(suffix):
return fill('slotname' + suffix, 11)

assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK
assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK
PWS_slot_count = 16
for i in range(0, PWS_slot_count):
iss = str(i)
assert C.NK_write_password_safe_slot(i,
get_slotname(iss), get_loginname(iss),
get_pass(iss)) == DeviceErrorCode.STATUS_OK
helper_PWS_get_slotname(iss), helper_PWS_get_loginname(iss),
helper_PWS_get_pass(iss)) == DeviceErrorCode.STATUS_OK

for j in range(0, 10):
for i in range(0, PWS_slot_count):
iss = str(i)
assert gs(C.NK_get_password_safe_slot_name(i)) == get_slotname(iss)
assert gs(C.NK_get_password_safe_slot_login(i)) == get_loginname(iss)
assert gs(C.NK_get_password_safe_slot_password(i)) == get_pass(iss)
assert gs(C.NK_get_password_safe_slot_name(i)) == helper_PWS_get_slotname(iss)
assert gs(C.NK_get_password_safe_slot_login(i)) == helper_PWS_get_loginname(iss)
assert gs(C.NK_get_password_safe_slot_password(i)) == helper_PWS_get_pass(iss)


@pytest.mark.lock_device
@pytest.mark.PWS
@pytest.mark.slowtest
@pytest.mark.xfail(reason="This test should be run directly after test_write_all_password_safe_slots_and_read_10_times")
def test_read_all_password_safe_slots_10_times(C):
def fill(s, wid):
assert wid >= len(s)
numbers = '1234567890'*4
s += numbers[:wid-len(s)]
assert len(s) == wid
return bb(s)

def get_pass(suffix):
return fill('pass' + suffix, 20)

def get_loginname(suffix):
return fill('login' + suffix, 32)

def get_slotname(suffix):
return fill('slotname' + suffix, 11)

assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK
assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK
PWS_slot_count = 16

for j in range(0, 10):
for i in range(0, PWS_slot_count):
iss = str(i)
assert gs(C.NK_get_password_safe_slot_name(i)) == get_slotname(iss)
assert gs(C.NK_get_password_safe_slot_login(i)) == get_loginname(iss)
assert gs(C.NK_get_password_safe_slot_password(i)) == get_pass(iss)
assert gs(C.NK_get_password_safe_slot_name(i)) == helper_PWS_get_slotname(iss)
assert gs(C.NK_get_password_safe_slot_login(i)) == helper_PWS_get_loginname(iss)
assert gs(C.NK_get_password_safe_slot_password(i)) == helper_PWS_get_pass(iss)


@pytest.mark.lock_device
Expand Down Expand Up @@ -977,31 +946,6 @@ def test_get_device_model(C):
# assert C.NK_get_device_model() != C.NK_DISCONNECTED


@pytest.mark.firmware
def test_bootloader_password_change_pro(C):
skip_if_device_version_lower_than({'P': 11})
assert C.NK_change_firmware_password_pro(b'zxcasd', b'zxcasd') == DeviceErrorCode.WRONG_PASSWORD

assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.STATUS_OK
assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_TEMP, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK


@pytest.mark.firmware
def test_bootloader_run_pro(C):
skip_if_device_version_lower_than({'P': 11})
assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.WRONG_PASSWORD
# Not enabled due to lack of side-effect removal at this point
# assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK


@pytest.mark.firmware
def test_bootloader_password_change_pro_too_long(C):
skip_if_device_version_lower_than({'P': 11})
long_string = b'a' * 100
assert C.NK_change_firmware_password_pro(long_string, long_string) == LibraryErrors.TOO_LONG_STRING
assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, long_string) == LibraryErrors.TOO_LONG_STRING


@pytest.mark.otp
@pytest.mark.parametrize('counter_mid', [10**3-1, 10**4-1, 10**7-1, 10**8-10, 2**16, 2**31-1, 2**32-1, 2**33, 2**50, 2**60, 2**63]) # 2**64-1
def test_HOTP_counter_getter(C, counter_mid: int):
Expand Down
Loading

0 comments on commit 6100df4

Please sign in to comment.