198 lines
6.8 KiB
Python
198 lines
6.8 KiB
Python
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# Test PIN code management. Requires real device, emulator is useless for this.
|
|
#
|
|
# CAUTIONS:
|
|
# - interrupting these test can leave unit in difficult-to-recover states
|
|
# - will erase seed
|
|
# - assumes no PIN set yet
|
|
# - dev mode must be enabled
|
|
# - these tests need to run individually, not working well all together
|
|
# - provide "--mk 3" on command line for newer hardware stuff
|
|
# - always run with "-s" so you have something to watch: very slow.
|
|
# - mark2 no longer supported here, assumes mk3+, nor "secondary" pin's
|
|
# - mark4 works over USB protocol, so must be logged in so can't be empty pin
|
|
#
|
|
import time, pytest, os
|
|
from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError, MAX_TXN_LEN, CCUserRefused
|
|
from binascii import b2a_hex, a2b_hex
|
|
from pprint import pprint
|
|
|
|
@pytest.fixture(scope='module')
|
|
def setup_repl(repl):
|
|
repl.exec('from glob import dis; from pincodes import pa; import callgate')
|
|
|
|
def xxx_test_repl(repl, setup_repl):
|
|
# check repl works
|
|
resp = repl.exec('print("hello")')
|
|
assert resp == 'hello\r\n'
|
|
|
|
def test_eval(repl):
|
|
assert repl.eval('1+2') == 3
|
|
assert repl.eval("'a'+'b'") == "ab"
|
|
|
|
@pytest.mark.parametrize('pin', [ '12-12', '123456-123456'])
|
|
def test_pin_set(repl, setup_repl, pin, only_mk3):
|
|
# always clear it after!
|
|
# might need for setup/recovery:
|
|
# pa.setup(b'12-12'); pa.login(); pa.change(new_pin=b'')
|
|
#
|
|
# Mk4: broken, because can't do anything w/ empty pin over USB
|
|
|
|
assert pin != ''
|
|
|
|
assert repl.eval("pa.setup(b'')")&0xf == 3, 'pin wasnt blank'
|
|
#assert repl.eval("pa.login() if not pa.is_blank() else True") == True
|
|
|
|
print("Attempt pin set to: %s" % pin)
|
|
assert repl.eval("pa.change(new_pin=b'%s')" % pin) == None
|
|
|
|
assert repl.eval("pa.setup(b'%s')" % pin)&0xf == 0
|
|
|
|
assert repl.eval('pa.private_state == 0') == True
|
|
|
|
assert repl.eval("pa.login()") == True
|
|
|
|
assert repl.eval('pa.private_state != 0') == True
|
|
|
|
assert repl.eval("pa.change(new_pin=b'')") == None
|
|
|
|
# this line is a bugfix: mk1/2 bootroms need login after pin change
|
|
assert repl.eval("pa.setup(b'')") == 3
|
|
|
|
time.sleep(1)
|
|
|
|
@pytest.mark.parametrize('test_secret', [b'a'*72, b'X'*72, b'\0'*72])
|
|
def test_set_secret(repl, setup_repl, test_secret):
|
|
assert repl.eval('pa.is_successful()'), 'not logged in?'
|
|
|
|
assert repl.eval("pa.change(new_secret=%r)" % test_secret) == None
|
|
assert repl.eval("pa.fetch()") == test_secret
|
|
|
|
# recovery time, so USB port can service traffic?
|
|
time.sleep(1)
|
|
|
|
def test_prefix_words(repl, setup_repl):
|
|
# NOTE: doing more than 10 these, we get tarpitted, and more than 25 will cause lockup
|
|
# - all units will have different results here
|
|
a1 = repl.eval("pa.prefix_words(b'12-')")
|
|
a2 = repl.eval("pa.prefix_words(b'435-')")
|
|
assert a1 != a2
|
|
a3 = repl.eval("pa.prefix_words(b'12-')")
|
|
assert a3 != a2
|
|
assert a1 == a3
|
|
|
|
def test_greenlight(repl, setup_repl, is_mark4):
|
|
from random import randint
|
|
|
|
# NOTE: the return values and names of these functions are all stupid.
|
|
|
|
assert repl.eval("pa.greenlight_firmware()", max_time=5) == None
|
|
|
|
assert repl.eval("callgate.get_genuine()") == 1
|
|
assert repl.eval("callgate.clear_genuine()") == None
|
|
assert repl.eval("callgate.get_genuine()") == 0
|
|
|
|
assert repl.eval("callgate.set_genuine()", max_time=5) == 0
|
|
assert repl.eval("callgate.get_genuine()") == 1
|
|
|
|
# 'set_genuine' really means "test if genuine" here
|
|
if is_mark4:
|
|
# TODO: need a way to break 'genuine' state on mk4?
|
|
assert repl.eval("callgate.set_genuine()", max_time=5) == 0
|
|
else:
|
|
# this changes flash and so affects genuine status
|
|
assert repl.eval("open('/flash/test', 'wb').write(b'hi %06d')" % randint(1,1e6)) >= 3
|
|
time.sleep(1)
|
|
|
|
assert repl.eval("callgate.set_genuine()", max_time=5) == -1
|
|
|
|
assert repl.eval("callgate.get_genuine()") == 1
|
|
|
|
assert repl.eval("pa.greenlight_firmware()", max_time=5) == None
|
|
|
|
assert repl.eval("callgate.set_genuine()", max_time=5) == 0
|
|
assert repl.eval("callgate.get_genuine()") == 1
|
|
|
|
repl.exec("dis.clear(); dis.text(0,0, 'done'); dis.show()")
|
|
|
|
def test_duress(repl, setup_repl, only_mk3):
|
|
ss = repl.eval("pa.setup(b'')")
|
|
assert ss&0xf == 3
|
|
|
|
assert repl.eval('pa.private_state == 0') == False
|
|
assert repl.eval('pa.has_duress_pin()') == False
|
|
assert repl.eval('pa.is_successful()') == True
|
|
assert repl.eval("pa.change(is_duress=True, new_pin=b'34-34', old_pin=b'')") == None
|
|
assert repl.eval("pa.change(is_duress=True, new_secret=b'a'*72, old_pin=b'34-34')") == None
|
|
assert repl.eval("pa.fetch(duress_pin=b'34-34')") == b'a'*72
|
|
assert repl.eval("pa.change(is_duress=True, new_secret=bytes(72), old_pin=b'34-34', new_pin=b'')") == None
|
|
assert repl.eval('pa.has_duress_pin()') == False
|
|
|
|
# cleanup
|
|
repl.eval("pa.setup(b'')")
|
|
|
|
MAX_ATT = 13
|
|
|
|
@pytest.mark.parametrize('nfails', [MAX_ATT-1, 1, 3, 5])
|
|
def test_bad_logins(repl, setup_repl, nfails):
|
|
ss = repl.eval("pa.setup(b'')")
|
|
|
|
if ss&0xf != 3:
|
|
# robustness: recover w/ probable pin
|
|
repl.eval("pa.setup(b'12-12')")
|
|
assert repl.eval("pa.login()") == True
|
|
|
|
assert repl.eval("pa.change(new_pin=b'12-12')") == None
|
|
assert repl.eval("pa.setup(b'12-12')")&0xf == 0
|
|
assert repl.eval("pa.login()") == True
|
|
|
|
def prepare_attempt(pin):
|
|
assert repl.eval("pa.setup(%r)" % pin)&0xf == 0
|
|
|
|
nd, nf, al = repl.eval('pa.delay_required, pa.num_fails, pa.attempts_left')
|
|
assert nd == 0 # must be zero, obsolete
|
|
assert al <= MAX_ATT
|
|
assert nf + al == MAX_ATT
|
|
|
|
return nf
|
|
|
|
# try wrong pin a few times
|
|
for n in range(nfails):
|
|
nf = prepare_attempt(b'xx')
|
|
|
|
assert nf == n
|
|
|
|
with pytest.raises(RuntimeError) as ee:
|
|
repl.eval("pa.login()")
|
|
assert 'AUTH_FAIL' in ee.value.args[0]
|
|
|
|
# should be successful now
|
|
prepare_attempt(b'12-12')
|
|
assert repl.eval("pa.login()") == True
|
|
|
|
nf, al = repl.eval('pa.num_fails, pa.attempts_left')
|
|
assert nf == 0
|
|
assert al == MAX_ATT
|
|
|
|
# reset state
|
|
assert repl.eval("pa.change(new_pin=b'')") == None
|
|
assert repl.eval("pa.setup(b'')")&0xf == 3
|
|
nf, al = repl.eval('pa.num_fails, pa.attempts_left')
|
|
assert nf == 0
|
|
assert al == MAX_ATT
|
|
|
|
@pytest.mark.parametrize('test_secret', [b'a'*416, b'\0'*32+b'm'*(416-32),
|
|
bytearray(0x41+(i%57) for i in range(416))])
|
|
def test_long_secret(repl, setup_repl, test_secret):
|
|
assert repl.eval('pa.is_successful()'), 'not logged in?'
|
|
|
|
assert repl.eval("pa.ls_change(%r)" % test_secret) == None
|
|
assert repl.eval("pa.ls_fetch()") == test_secret
|
|
|
|
# recovery time, so USB port can service traffic?
|
|
time.sleep(1)
|
|
|
|
|
|
# EOF
|