commit
f8f895df4f
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@ -0,0 +1,19 @@
|
||||
https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties
|
||||
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -74,7 +74,8 @@ tools/python-fido2/*
|
||||
*.key
|
||||
site/
|
||||
_site/
|
||||
venv/
|
||||
env2/
|
||||
env3/
|
||||
.project
|
||||
.tags*
|
||||
targets/*/docs/
|
||||
|
36
Makefile
36
Makefile
@ -7,7 +7,7 @@
|
||||
#define uECC_arm64 6
|
||||
#define uECC_avr 7
|
||||
|
||||
platform=2
|
||||
ecc_platform=2
|
||||
|
||||
EFM32_DEBUGGER= -s 440083537 --device EFM32JG1B200F128GM32
|
||||
#EFM32_DEBUGGER= -s 440121060 #dev board
|
||||
@ -68,29 +68,33 @@ $(name): $(obj) $(LIBCBOR)
|
||||
$(CC) $(LDFLAGS) -o $@ $(obj) $(LDFLAGS)
|
||||
|
||||
uECC.o: ./crypto/micro-ecc/uECC.c
|
||||
$(CC) -c -o $@ $^ -O2 -fdata-sections -ffunction-sections -DuECC_PLATFORM=$(platform) -I./crypto/micro-ecc/
|
||||
$(CC) -c -o $@ $^ -O2 -fdata-sections -ffunction-sections -DuECC_PLATFORM=$(ecc_platform) -I./crypto/micro-ecc/
|
||||
|
||||
env2:
|
||||
virtualenv --python=python2.7 env2
|
||||
env2/bin/pip install --upgrade -r tools/requirements.txt
|
||||
|
||||
# python virtualenv
|
||||
env3:
|
||||
python3 -m venv env3
|
||||
env3/bin/pip install --upgrade -r tools/requirements.txt
|
||||
env3/bin/pip install --upgrade black
|
||||
|
||||
venv:
|
||||
@if ! which virtualenv >/dev/null ; then \
|
||||
echo "ERR: Sorry, no python virtualenv found. Please consider installing " ;\
|
||||
echo " it via something like:" ;\
|
||||
echo " sudo apt install python-virtualenv" ;\
|
||||
echo " or maybe:" ;\
|
||||
echo " pip install virtualenv" ;\
|
||||
fi
|
||||
virtualenv venv
|
||||
./venv/bin/pip install wheel
|
||||
# selectively reformat our own code
|
||||
black: env3
|
||||
env3/bin/black --skip-string-normalization tools/
|
||||
|
||||
wink2: env2
|
||||
env2/bin/python tools/solotool.py solo --wink
|
||||
|
||||
.PHONY: fido2-test
|
||||
fido2-test:
|
||||
./venv/bin/python tools/ctap_test.py
|
||||
wink3: env3
|
||||
env3/bin/python tools/solotool.py solo --wink
|
||||
|
||||
fido2-test: env3
|
||||
env3/bin/python tools/ctap_test.py
|
||||
|
||||
clean:
|
||||
rm -f *.o main.exe main $(obj)
|
||||
rm -rf env2 env3
|
||||
for f in crypto/tiny-AES-c/Makefile tinycbor/Makefile ; do \
|
||||
if [ -f "$$f" ]; then \
|
||||
(cd `dirname $$f` ; git checkout -- .) ;\
|
||||
|
@ -47,6 +47,7 @@ make all-hacker
|
||||
python ../../tools/programmer.py solo.hex
|
||||
```
|
||||
|
||||
If you forgot the `--recurse-submodules` when cloning, simply `git submodule update --init --recursive`.
|
||||
For example, if you want to turn off any blue light emission, you can edit [`led_rgb()`](https://github.com/SoloKeysSec/solo/blob/master/targets/stm32l432/src/led.c#L15) and force:
|
||||
```
|
||||
uint32_t b = 0;
|
||||
|
6
tools/convert_log_to_c.py
Normal file → Executable file
6
tools/convert_log_to_c.py
Normal file → Executable file
@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -23,7 +25,7 @@ import sys
|
||||
from sys import argv
|
||||
|
||||
if len(argv) != 2:
|
||||
print("usage: %s <input-log>" % argv[0]);
|
||||
print("usage: %s <input-log>" % argv[0])
|
||||
sys.exit(1)
|
||||
|
||||
log = open(argv[1]).readlines()
|
||||
@ -40,7 +42,7 @@ for x in log:
|
||||
pass
|
||||
if len(parse) == 0:
|
||||
continue
|
||||
assert(len(parse) == 64)
|
||||
assert len(parse) == 64
|
||||
nums.append(parse)
|
||||
|
||||
hexlines = []
|
||||
|
234
tools/ctap_test.py
Normal file → Executable file
234
tools/ctap_test.py
Normal file → Executable file
@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -58,7 +60,8 @@ class Packet(object):
|
||||
def FromWireFormat(pkt_size, data):
|
||||
return Packet(data)
|
||||
|
||||
class Tester():
|
||||
|
||||
class Tester:
|
||||
def __init__(self,):
|
||||
self.origin = 'https://examplo.org'
|
||||
self.host = 'examplo.org'
|
||||
@ -95,7 +98,7 @@ class Tester():
|
||||
pad = struct.pack('%dB' % len(pad), *[ord(x) for x in pad])
|
||||
data = data + pad
|
||||
data = list(data)
|
||||
assert(len(data) == 64)
|
||||
assert len(data) == 64
|
||||
self.dev._dev.InternalSendPacket(Packet(data))
|
||||
|
||||
def cid(self,):
|
||||
@ -112,7 +115,7 @@ class Tester():
|
||||
return cmd, payload
|
||||
|
||||
def check_error(self, data, err=None):
|
||||
assert(len(data) == 1)
|
||||
assert len(data) == 1
|
||||
if err is None:
|
||||
if data[0] != 0:
|
||||
raise CtapError(data[0])
|
||||
@ -129,9 +132,9 @@ class Tester():
|
||||
delt = t2 - t1
|
||||
# if (delt < 140 ):
|
||||
# raise RuntimeError('Fob is too fast (%d ms)' % delt)
|
||||
if (delt > 555 * (amt/1000)):
|
||||
if delt > 555 * (amt / 1000):
|
||||
raise RuntimeError('Fob is too slow (%d ms)' % delt)
|
||||
if (r != pingdata):
|
||||
if r != pingdata:
|
||||
raise ValueError('Ping data not echo\'d')
|
||||
print('1000 byte ping time: %s ms' % delt)
|
||||
except CtapError as e:
|
||||
@ -141,7 +144,6 @@ class Tester():
|
||||
# sys.flush(sys.sto)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def test_hid(self, check_timeouts=False):
|
||||
if check_timeouts:
|
||||
print('Test idle')
|
||||
@ -153,11 +155,10 @@ class Tester():
|
||||
print('Test init')
|
||||
r = self.send_data(CTAPHID.INIT, '\x11\x11\x11\x11\x11\x11\x11\x11')
|
||||
|
||||
|
||||
pingdata = os.urandom(100)
|
||||
try:
|
||||
r = self.send_data(CTAPHID.PING, pingdata)
|
||||
if (r != pingdata):
|
||||
if r != pingdata:
|
||||
raise ValueError('Ping data not echo\'d')
|
||||
except CtapError as e:
|
||||
print('100 byte Ping failed:', e)
|
||||
@ -187,7 +188,7 @@ class Tester():
|
||||
if len(r) > 1 or r[0] == 0:
|
||||
raise RuntimeError('Cbor is supposed to have payload')
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.INVALID_LENGTH)
|
||||
assert e.code == CtapError.ERR.INVALID_LENGTH
|
||||
print('PASS: no data cbor')
|
||||
|
||||
try:
|
||||
@ -196,7 +197,7 @@ class Tester():
|
||||
if len(r) > 2:
|
||||
raise RuntimeError('MSG is supposed to have payload')
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.INVALID_LENGTH)
|
||||
assert e.code == CtapError.ERR.INVALID_LENGTH
|
||||
print('PASS: no data msg')
|
||||
|
||||
try:
|
||||
@ -209,10 +210,9 @@ class Tester():
|
||||
r = self.send_data(0x66, '')
|
||||
raise RuntimeError('Invalid command did not return error')
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.INVALID_COMMAND)
|
||||
assert e.code == CtapError.ERR.INVALID_COMMAND
|
||||
print('PASS: invalid HID command')
|
||||
|
||||
|
||||
print('Sending packet with too large of a length.')
|
||||
self.send_raw('\x81\x1d\xba\x00')
|
||||
cmd, resp = self.recv_raw()
|
||||
@ -230,7 +230,7 @@ class Tester():
|
||||
self.check_error(resp, CtapError.ERR.INVALID_SEQ)
|
||||
if check_timeouts:
|
||||
cmd, resp = self.recv_raw()
|
||||
assert(cmd == 0xbf) # timeout
|
||||
assert cmd == 0xBF # timeout
|
||||
print('PASS: invalid sequence')
|
||||
|
||||
print('Resync and send ping')
|
||||
@ -238,7 +238,7 @@ class Tester():
|
||||
r = self.send_data(CTAPHID.INIT, '\x11\x22\x33\x44\x55\x66\x77\x88')
|
||||
pingdata = os.urandom(100)
|
||||
r = self.send_data(CTAPHID.PING, pingdata)
|
||||
if (r != pingdata):
|
||||
if r != pingdata:
|
||||
raise ValueError('Ping data not echo\'d')
|
||||
except CtapError as e:
|
||||
raise RuntimeError('resync fail: ', e)
|
||||
@ -261,15 +261,17 @@ class Tester():
|
||||
self.send_raw('\x00')
|
||||
self.send_raw('\x01')
|
||||
self.set_cid(newcid)
|
||||
self.send_raw('\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88') # init from different cid
|
||||
self.send_raw(
|
||||
'\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88'
|
||||
) # init from different cid
|
||||
print('wait for init response')
|
||||
cmd, r = self.recv_raw() # init response
|
||||
assert(cmd == 0x86)
|
||||
assert cmd == 0x86
|
||||
self.set_cid(oldcid)
|
||||
if check_timeouts:
|
||||
# print('wait for timeout')
|
||||
cmd, r = self.recv_raw() # timeout response
|
||||
assert(cmd == 0xbf)
|
||||
assert cmd == 0xBF
|
||||
|
||||
print('PASS: resync and timeout')
|
||||
|
||||
@ -282,9 +284,9 @@ class Tester():
|
||||
cmd, r = self.recv_raw() # timeout response
|
||||
t2 = time.time() * 1000
|
||||
delt = t2 - t1
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.TIMEOUT)
|
||||
assert(delt < 1000 and delt > 400)
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.TIMEOUT
|
||||
assert delt < 1000 and delt > 400
|
||||
print('Pass timeout')
|
||||
|
||||
print('Test not cont')
|
||||
@ -294,8 +296,8 @@ class Tester():
|
||||
self.send_raw('\x01')
|
||||
self.send_raw('\x81\x10\x00') # init packet
|
||||
cmd, r = self.recv_raw() # timeout response
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.INVALID_SEQ)
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.INVALID_SEQ
|
||||
print('PASS: Test not cont')
|
||||
|
||||
if check_timeouts:
|
||||
@ -318,14 +320,14 @@ class Tester():
|
||||
self.send_raw('\x81\x04\x00')
|
||||
cmd, r = self.recv_raw() # busy response
|
||||
t2 = time.time() * 1000
|
||||
assert(t2-t1 < 100)
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.CHANNEL_BUSY)
|
||||
assert t2 - t1 < 100
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.CHANNEL_BUSY
|
||||
|
||||
self.set_cid(oldcid)
|
||||
cmd, r = self.recv_raw() # timeout response
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.TIMEOUT)
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.TIMEOUT
|
||||
print('PASS: busy')
|
||||
|
||||
print('Check busy interleaved')
|
||||
@ -348,18 +350,18 @@ class Tester():
|
||||
|
||||
self.set_cid(cid2)
|
||||
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.CHANNEL_BUSY)
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.CHANNEL_BUSY
|
||||
|
||||
self.set_cid(cid1)
|
||||
cmd, r = self.recv_raw() # ping response
|
||||
assert(cmd == 0x81)
|
||||
assert(len(r) == 0x63)
|
||||
assert cmd == 0x81
|
||||
assert len(r) == 0x63
|
||||
|
||||
if check_timeouts:
|
||||
cmd, r = self.recv_raw() # timeout
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.TIMEOUT)
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.TIMEOUT
|
||||
print('PASS: busy interleaved')
|
||||
|
||||
if check_timeouts:
|
||||
@ -372,18 +374,22 @@ class Tester():
|
||||
|
||||
print('Test cid 0 is invalid')
|
||||
self.set_cid('\x00\x00\x00\x00')
|
||||
self.send_raw('\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88', cid = '\x00\x00\x00\x00')
|
||||
self.send_raw(
|
||||
'\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88', cid='\x00\x00\x00\x00'
|
||||
)
|
||||
cmd, r = self.recv_raw() # timeout
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.INVALID_CHANNEL)
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.INVALID_CHANNEL
|
||||
print('Pass: cid 0')
|
||||
|
||||
print('Test invalid broadcast cid use')
|
||||
self.set_cid('\xff\xff\xff\xff')
|
||||
self.send_raw('\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88', cid = '\xff\xff\xff\xff')
|
||||
self.send_raw(
|
||||
'\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88', cid='\xff\xff\xff\xff'
|
||||
)
|
||||
cmd, r = self.recv_raw() # timeout
|
||||
assert(cmd == 0xbf)
|
||||
assert(r[0] == CtapError.ERR.INVALID_CHANNEL)
|
||||
assert cmd == 0xBF
|
||||
assert r[0] == CtapError.ERR.INVALID_CHANNEL
|
||||
print('Pass: cid broadcast')
|
||||
|
||||
def test_u2f(self,):
|
||||
@ -405,7 +411,9 @@ class Tester():
|
||||
|
||||
print('MC')
|
||||
t1 = time.time() * 1000
|
||||
attest, data = self.client.make_credential(rp, user, challenge, pin = PIN, exclude_list = [])
|
||||
attest, data = self.client.make_credential(
|
||||
rp, user, challenge, pin=PIN, exclude_list=[]
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
attest.verify(data.hash)
|
||||
print('Register valid (%d ms)' % (t2 - t1))
|
||||
@ -415,7 +423,9 @@ class Tester():
|
||||
|
||||
allow_list = [{'id': creds[0].credential_id, 'type': 'public-key'}]
|
||||
t1 = time.time() * 1000
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, allow_list, pin = PIN)
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, allow_list, pin=PIN
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
assertions[0].verify(client_data.hash, creds[0].public_key)
|
||||
|
||||
@ -437,8 +447,12 @@ class Tester():
|
||||
|
||||
challenge = ''.join([abc[randint(0, len(abc) - 1)] for x in range(0, 32)])
|
||||
|
||||
fake_id1 = array.array('B',[randint(0,255) for i in range(0,150)]).tostring()
|
||||
fake_id2 = array.array('B',[randint(0,255) for i in range(0,73)]).tostring()
|
||||
fake_id1 = array.array(
|
||||
'B', [randint(0, 255) for i in range(0, 150)]
|
||||
).tostring()
|
||||
fake_id2 = array.array(
|
||||
'B', [randint(0, 255) for i in range(0, 73)]
|
||||
).tostring()
|
||||
|
||||
exclude_list.append({'id': fake_id1, 'type': 'public-key'})
|
||||
exclude_list.append({'id': fake_id2, 'type': 'public-key'})
|
||||
@ -446,7 +460,9 @@ class Tester():
|
||||
# for i in range(0,2048**2):
|
||||
for i in range(0, 1):
|
||||
t1 = time.time() * 1000
|
||||
attest, data = self.client.make_credential(rp, user, challenge, pin = PIN, exclude_list = [])
|
||||
attest, data = self.client.make_credential(
|
||||
rp, user, challenge, pin=PIN, exclude_list=[]
|
||||
)
|
||||
print(attest.auth_data.counter)
|
||||
t2 = time.time() * 1000
|
||||
attest.verify(data.hash)
|
||||
@ -460,7 +476,9 @@ class Tester():
|
||||
for i in range(0, 1):
|
||||
allow_list = [{'id': creds[0].credential_id, 'type': 'public-key'}]
|
||||
t1 = time.time() * 1000
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, allow_list, pin = PIN)
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, allow_list, pin=PIN
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
assertions[0].verify(client_data.hash, creds[0].public_key)
|
||||
print(assertions[0].auth_data.counter)
|
||||
@ -468,8 +486,6 @@ class Tester():
|
||||
print('Assertion valid (%d ms)' % (t2 - t1))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
|
||||
def test_fido2(self):
|
||||
def test(self, pincode=None):
|
||||
creds = []
|
||||
@ -479,8 +495,12 @@ class Tester():
|
||||
challenge = 'Y2hhbGxlbmdl'
|
||||
PIN = pincode
|
||||
|
||||
fake_id1 = array.array('B',[randint(0,255) for i in range(0,150)]).tostring()
|
||||
fake_id2 = array.array('B',[randint(0,255) for i in range(0,73)]).tostring()
|
||||
fake_id1 = array.array(
|
||||
'B', [randint(0, 255) for i in range(0, 150)]
|
||||
).tostring()
|
||||
fake_id2 = array.array(
|
||||
'B', [randint(0, 255) for i in range(0, 73)]
|
||||
).tostring()
|
||||
|
||||
exclude_list.append({'id': fake_id1, 'type': 'public-key'})
|
||||
exclude_list.append({'id': fake_id2, 'type': 'public-key'})
|
||||
@ -488,7 +508,9 @@ class Tester():
|
||||
# test make credential
|
||||
print('make 3 credentials')
|
||||
for i in range(0, 3):
|
||||
attest, data = self.client.make_credential(rp, user, challenge, pin = PIN, exclude_list = [])
|
||||
attest, data = self.client.make_credential(
|
||||
rp, user, challenge, pin=PIN, exclude_list=[]
|
||||
)
|
||||
attest.verify(data.hash)
|
||||
cred = attest.auth_data.credential_data
|
||||
creds.append(cred)
|
||||
@ -498,15 +520,19 @@ class Tester():
|
||||
if PIN is not None:
|
||||
print('make credential with wrong pin code')
|
||||
try:
|
||||
attest, data = self.client.make_credential(rp, user, challenge, pin = PIN + ' ', exclude_list = [])
|
||||
attest, data = self.client.make_credential(
|
||||
rp, user, challenge, pin=PIN + ' ', exclude_list=[]
|
||||
)
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.PIN_INVALID)
|
||||
assert e.code == CtapError.ERR.PIN_INVALID
|
||||
except ClientError as e:
|
||||
assert(e.cause.code == CtapError.ERR.PIN_INVALID)
|
||||
assert e.cause.code == CtapError.ERR.PIN_INVALID
|
||||
print('PASS')
|
||||
|
||||
print('make credential with exclude list')
|
||||
attest, data = self.client.make_credential(rp, user, challenge, pin = PIN, exclude_list = exclude_list)
|
||||
attest, data = self.client.make_credential(
|
||||
rp, user, challenge, pin=PIN, exclude_list=exclude_list
|
||||
)
|
||||
attest.verify(data.hash)
|
||||
cred = attest.auth_data.credential_data
|
||||
creds.append(cred)
|
||||
@ -515,35 +541,42 @@ class Tester():
|
||||
print('make credential with exclude list including real credential')
|
||||
real_excl = [{'id': cred.credential_id, 'type': 'public-key'}]
|
||||
try:
|
||||
attest, data = self.client.make_credential(rp, user, challenge, pin = PIN, exclude_list = exclude_list + real_excl)
|
||||
attest, data = self.client.make_credential(
|
||||
rp, user, challenge, pin=PIN, exclude_list=exclude_list + real_excl
|
||||
)
|
||||
raise RuntimeError('Exclude list did not return expected error')
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.CREDENTIAL_EXCLUDED)
|
||||
assert e.code == CtapError.ERR.CREDENTIAL_EXCLUDED
|
||||
except ClientError as e:
|
||||
assert(e.cause.code == CtapError.ERR.CREDENTIAL_EXCLUDED)
|
||||
assert e.cause.code == CtapError.ERR.CREDENTIAL_EXCLUDED
|
||||
print('PASS')
|
||||
|
||||
for i, x in enumerate(creds):
|
||||
print('get assertion %d' % i)
|
||||
allow_list = [{'id': x.credential_id, 'type': 'public-key'}]
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, allow_list, pin = PIN)
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, allow_list, pin=PIN
|
||||
)
|
||||
assertions[0].verify(client_data.hash, x.public_key)
|
||||
print('PASS')
|
||||
|
||||
if PIN is not None:
|
||||
print('get assertion with wrong pin code')
|
||||
try:
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, allow_list, pin = PIN + ' ')
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, allow_list, pin=PIN + ' '
|
||||
)
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.PIN_INVALID)
|
||||
assert e.code == CtapError.ERR.PIN_INVALID
|
||||
except ClientError as e:
|
||||
assert(e.cause.code == CtapError.ERR.PIN_INVALID)
|
||||
assert e.cause.code == CtapError.ERR.PIN_INVALID
|
||||
print('PASS')
|
||||
|
||||
|
||||
print('get multiple assertions')
|
||||
allow_list = [{'id': x.credential_id, 'type': 'public-key'} for x in creds]
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, allow_list, pin = PIN)
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, allow_list, pin=PIN
|
||||
)
|
||||
|
||||
for ass, cred in zip(assertions, creds):
|
||||
i += 1
|
||||
@ -571,7 +604,7 @@ class Tester():
|
||||
try:
|
||||
self.client.pin_protocol.set_pin(PIN)
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.NOT_ALLOWED)
|
||||
assert e.code == CtapError.ERR.NOT_ALLOWED
|
||||
print('PASS')
|
||||
|
||||
print('Change pin code')
|
||||
@ -584,21 +617,21 @@ class Tester():
|
||||
try:
|
||||
self.client.pin_protocol.change_pin(PIN.replace('a', 'b'), '1234')
|
||||
except CtapError as e:
|
||||
assert(e.code == CtapError.ERR.PIN_INVALID)
|
||||
assert e.code == CtapError.ERR.PIN_INVALID
|
||||
print('PASS')
|
||||
|
||||
print('MC using wrong pin')
|
||||
try:
|
||||
self.test_fido2_simple('abcd3');
|
||||
self.test_fido2_simple('abcd3')
|
||||
except ClientError as e:
|
||||
assert(e.cause.code == CtapError.ERR.PIN_INVALID)
|
||||
assert e.cause.code == CtapError.ERR.PIN_INVALID
|
||||
print('PASS')
|
||||
|
||||
print('get info')
|
||||
inf = self.ctap.get_info()
|
||||
print('PASS')
|
||||
|
||||
self.test_fido2_simple(PIN);
|
||||
self.test_fido2_simple(PIN)
|
||||
|
||||
print('Re-run make_credential and get_assertion tests with pin code')
|
||||
test(self, PIN)
|
||||
@ -615,7 +648,9 @@ class Tester():
|
||||
rp = {'id': self.host, 'name': 'ExaRP'}
|
||||
user0 = {'id': b'first one', 'name': 'single User'}
|
||||
|
||||
users = [{'id': b'user' + os.urandom(16), 'name': 'AB User'} for i in range(0,2)]
|
||||
users = [
|
||||
{'id': b'user' + os.urandom(16), 'name': 'AB User'} for i in range(0, 2)
|
||||
]
|
||||
challenge = 'Y2hhbGxlbmdl'
|
||||
PIN = None
|
||||
print('reset')
|
||||
@ -624,7 +659,9 @@ class Tester():
|
||||
|
||||
print('registering 1 user with RK')
|
||||
t1 = time.time() * 1000
|
||||
attest, data = self.client.make_credential(rp, user0, challenge, pin = PIN, exclude_list = [], rk = True)
|
||||
attest, data = self.client.make_credential(
|
||||
rp, user0, challenge, pin=PIN, exclude_list=[], rk=True
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
attest.verify(data.hash)
|
||||
creds.append(attest.auth_data.credential_data)
|
||||
@ -632,27 +669,31 @@ class Tester():
|
||||
|
||||
print('1 assertion')
|
||||
t1 = time.time() * 1000
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, pin = PIN)
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, pin=PIN
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
assertions[0].verify(client_data.hash, creds[0].public_key)
|
||||
print('Assertion valid (%d ms)' % (t2 - t1))
|
||||
|
||||
print(assertions[0], client_data)
|
||||
|
||||
|
||||
print('registering %d users with RK' % len(users))
|
||||
for i in range(0, len(users)):
|
||||
t1 = time.time() * 1000
|
||||
attest, data = self.client.make_credential(rp, users[i], challenge, pin = PIN, exclude_list = [], rk = True)
|
||||
attest, data = self.client.make_credential(
|
||||
rp, users[i], challenge, pin=PIN, exclude_list=[], rk=True
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
attest.verify(data.hash)
|
||||
print('Register valid (%d ms)' % (t2 - t1))
|
||||
|
||||
creds.append(attest.auth_data.credential_data)
|
||||
|
||||
|
||||
t1 = time.time() * 1000
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, pin = PIN)
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, pin=PIN
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
|
||||
for x, y in zip(assertions, creds):
|
||||
@ -660,38 +701,41 @@ class Tester():
|
||||
|
||||
print('Assertion(s) valid (%d ms)' % (t2 - t1))
|
||||
|
||||
|
||||
print('registering a duplicate user ')
|
||||
|
||||
t1 = time.time() * 1000
|
||||
attest, data = self.client.make_credential(rp, users[1], challenge, pin = PIN, exclude_list = [], rk = True)
|
||||
attest, data = self.client.make_credential(
|
||||
rp, users[1], challenge, pin=PIN, exclude_list=[], rk=True
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
attest.verify(data.hash)
|
||||
creds = creds[:2] + creds[3:] + [attest.auth_data.credential_data]
|
||||
print('Register valid (%d ms)' % (t2 - t1))
|
||||
|
||||
|
||||
t1 = time.time() * 1000
|
||||
assertions, client_data = self.client.get_assertion(rp['id'], challenge, pin = PIN)
|
||||
assertions, client_data = self.client.get_assertion(
|
||||
rp['id'], challenge, pin=PIN
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
assert(len(assertions) == len(users) +1)
|
||||
assert len(assertions) == len(users) + 1
|
||||
for x, y in zip(assertions, creds):
|
||||
x.verify(client_data.hash, y.public_key)
|
||||
|
||||
print('Assertion(s) valid (%d ms)' % (t2 - t1))
|
||||
|
||||
|
||||
def test_responses(self,):
|
||||
PIN = '1234'
|
||||
RPID = self.host
|
||||
for dev in (CtapHidDevice.list_devices()):
|
||||
for dev in CtapHidDevice.list_devices():
|
||||
print('dev', dev)
|
||||
client = Fido2Client(dev, RPID)
|
||||
ctap = client.ctap2
|
||||
# ctap.reset()
|
||||
try:
|
||||
if PIN: client.pin_protocol.set_pin(PIN)
|
||||
except:pass
|
||||
if PIN:
|
||||
client.pin_protocol.set_pin(PIN)
|
||||
except:
|
||||
pass
|
||||
|
||||
inf = ctap.get_info()
|
||||
# print (inf)
|
||||
@ -708,21 +752,26 @@ class Tester():
|
||||
challenge = 'Y2hhbGxlbmdl'
|
||||
|
||||
if 1:
|
||||
attest, data = client.make_credential(rp,
|
||||
user, challenge, exclude_list = [], pin = PIN, rk=True)
|
||||
attest, data = client.make_credential(
|
||||
rp, user, challenge, exclude_list=[], pin=PIN, rk=True
|
||||
)
|
||||
|
||||
cred = attest.auth_data.credential_data
|
||||
creds = [cred]
|
||||
|
||||
allow_list = [{'id': creds[0].credential_id, 'type': 'public-key'}]
|
||||
allow_list = []
|
||||
assertions, client_data = client.get_assertion(rp['id'], challenge, pin = PIN)
|
||||
assertions, client_data = client.get_assertion(
|
||||
rp['id'], challenge, pin=PIN
|
||||
)
|
||||
assertions[0].verify(client_data.hash, creds[0].public_key)
|
||||
|
||||
if 0:
|
||||
print('registering 1 user with RK')
|
||||
t1 = time.time() * 1000
|
||||
attest, data = client.make_credential(rp, user, challenge, pin = PIN, exclude_list = [], rk = True)
|
||||
attest, data = client.make_credential(
|
||||
rp, user, challenge, pin=PIN, exclude_list=[], rk=True
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
attest.verify(data.hash)
|
||||
creds = [attest.auth_data.credential_data]
|
||||
@ -730,15 +779,13 @@ class Tester():
|
||||
|
||||
print('1 assertion')
|
||||
t1 = time.time() * 1000
|
||||
assertions, client_data = client.get_assertion(rp['id'], challenge, pin = PIN)
|
||||
assertions, client_data = client.get_assertion(
|
||||
rp['id'], challenge, pin=PIN
|
||||
)
|
||||
t2 = time.time() * 1000
|
||||
assertions[0].verify(client_data.hash, creds[0].public_key)
|
||||
print('Assertion valid (%d ms)' % (t2 - t1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# print('fmt:',attest.fmt)
|
||||
# print('rp_id_hash',attest.auth_data.rp_id_hash)
|
||||
# print('flags:', hex(attest.auth_data.flags))
|
||||
@ -763,7 +810,6 @@ class Tester():
|
||||
# break
|
||||
|
||||
|
||||
|
||||
def test_find_brute_force():
|
||||
i = 0
|
||||
while 1:
|
||||
|
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -22,6 +23,7 @@
|
||||
#
|
||||
from __future__ import print_function
|
||||
import base64
|
||||
|
||||
"""
|
||||
cbytes.py
|
||||
|
||||
@ -49,7 +51,7 @@ size = len(buf)
|
||||
a = ''.join(map(lambda c: '\\x%02x' % c, buf))
|
||||
|
||||
for i in range(0, len(a), 80):
|
||||
c_str += ("\""+a[i:i+80]+"\"\n")
|
||||
c_str += "\"" + a[i : i + 80] + "\"\n"
|
||||
|
||||
if '-s' in sys.argv:
|
||||
print(c_str)
|
||||
|
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -22,6 +23,7 @@
|
||||
#
|
||||
from __future__ import print_function
|
||||
import sys, fileinput, binascii
|
||||
|
||||
try:
|
||||
import ecdsa
|
||||
except:
|
||||
@ -48,4 +50,3 @@ for d1 in it:
|
||||
cstr += '\\x' + d1 + d2
|
||||
|
||||
print('"%s"' % cstr)
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -33,5 +35,3 @@ print(''.join(['%02x'%c for c in sk.to_string()]))
|
||||
print()
|
||||
print('"\\x' + '\\x'.join(['%02x' % c for c in sk.to_string()]) + '"')
|
||||
print()
|
||||
|
||||
|
||||
|
28
tools/http2udb.py
Normal file → Executable file
28
tools/http2udb.py
Normal file → Executable file
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -44,6 +45,7 @@ udpport = 8111
|
||||
|
||||
HEX_FILE = '../efm32/GNU ARM v7.2.1 - Debug/EFM32.hex'
|
||||
|
||||
|
||||
def ForceU2F(client, device):
|
||||
client.ctap = CTAP1(device)
|
||||
client.pin_protocol = None
|
||||
@ -64,8 +66,6 @@ if __name__ == '__main__':
|
||||
print(e)
|
||||
|
||||
|
||||
|
||||
|
||||
def write(data):
|
||||
msg = from_websafe(data)
|
||||
msg = base64.b64decode(msg)
|
||||
@ -82,10 +82,11 @@ def write(data):
|
||||
# data = client_param + app_param + struct.pack('>B', len(key_handle)) + key_handle
|
||||
# msg = str(msg.decode())
|
||||
# print(msg.decode())
|
||||
s = ctap.authenticate(chal,appid,msg,)
|
||||
s = ctap.authenticate(chal, appid, msg)
|
||||
print(s)
|
||||
# sock.sendto(msg, ('127.0.0.1', udpport))
|
||||
|
||||
|
||||
def read():
|
||||
# msg = [0]*64
|
||||
pkt, _ = sock.recvfrom(1000)
|
||||
@ -95,6 +96,7 @@ def read():
|
||||
msg = to_websafe(pkt)
|
||||
return msg
|
||||
|
||||
|
||||
class UDPBridge(BaseHTTPRequestHandler):
|
||||
def end_headers(self):
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
@ -111,9 +113,13 @@ class UDPBridge(BaseHTTPRequestHandler):
|
||||
chal = b"\xf6\xa2\x3c\xa4\x0a\xf9\xda\xd4\x5f\xdc\xba\x7d\xc9\xde\xcb\xed\xb5\x84\x64\x3a\x4c\x9f\x44\xc2\x04\xb0\x17\xd7\xf4\x3e\xe0\x3f"
|
||||
appid = b'A' * 32
|
||||
|
||||
s = ctap.authenticate(chal,appid,msg,)
|
||||
s = ctap.authenticate(chal, appid, msg)
|
||||
|
||||
data = struct.pack('B',s.user_presence) + struct.pack('>L',s.counter) + s.signature
|
||||
data = (
|
||||
struct.pack('B', s.user_presence)
|
||||
+ struct.pack('>L', s.counter)
|
||||
+ s.signature
|
||||
)
|
||||
data = base64.b64encode(data).decode('ascii')
|
||||
data = to_websafe(data)
|
||||
data = json.dumps({'data': data})
|
||||
@ -124,7 +130,6 @@ class UDPBridge(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/json')
|
||||
@ -135,13 +140,17 @@ class UDPBridge(BaseHTTPRequestHandler):
|
||||
|
||||
self.wfile.write(json.dumps(msg).encode())
|
||||
|
||||
|
||||
try:
|
||||
server = HTTPServer(('', httpport), UDPBridge)
|
||||
print('Started httpserver on port ', httpport)
|
||||
|
||||
server.socket = ssl.wrap_socket (server.socket,
|
||||
server.socket = ssl.wrap_socket(
|
||||
server.socket,
|
||||
keyfile="../web/localhost.key",
|
||||
certfile='../web/localhost.crt', server_side=True)
|
||||
certfile='../web/localhost.crt',
|
||||
server_side=True,
|
||||
)
|
||||
|
||||
print('Saving signed firmware to firmware.json')
|
||||
msg = get_firmware_object("signing_key.pem", HEX_FILE)
|
||||
@ -153,4 +162,3 @@ try:
|
||||
|
||||
except KeyboardInterrupt:
|
||||
server.socket.close()
|
||||
|
||||
|
6
tools/nfcmon.py
Normal file → Executable file
6
tools/nfcmon.py
Normal file → Executable file
@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -24,10 +26,12 @@ from binascii import hexlify
|
||||
|
||||
import Chameleon
|
||||
|
||||
|
||||
def verboseLog(text):
|
||||
formatString = "[{}] {}"
|
||||
timeString = datetime.datetime.utcnow()
|
||||
print(formatString.format(timeString, text), )
|
||||
print(formatString.format(timeString, text))
|
||||
|
||||
|
||||
chameleon = Chameleon.Device(verboseLog)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
ecdsa
|
||||
intelhex
|
||||
pyserial
|
||||
python-fido2
|
||||
fido2
|
||||
pyusb
|
||||
|
210
tools/solotool.py
Normal file → Executable file
210
tools/solotool.py
Normal file → Executable file
@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
|
||||
#
|
||||
@ -49,13 +51,16 @@ def to_websafe(data):
|
||||
data = data.replace('=', '')
|
||||
return data
|
||||
|
||||
|
||||
def from_websafe(data):
|
||||
data = data.replace('-', '+')
|
||||
data = data.replace('_', '/')
|
||||
return data + '=='[: (3 * len(data)) % 4]
|
||||
|
||||
|
||||
def get_firmware_object(sk_name, hex_file):
|
||||
from ecdsa import SigningKey, NIST256p
|
||||
|
||||
sk = SigningKey.from_pem(open(sk_name).read())
|
||||
fw = open(hex_file, 'r').read()
|
||||
fw = base64.b64encode(fw.encode())
|
||||
@ -65,7 +70,7 @@ def get_firmware_object(sk_name, hex_file):
|
||||
# start of firmware and the size of the flash region allocated for it.
|
||||
# TODO put this somewhere else.
|
||||
START = ih.segments()[0][0]
|
||||
END = ((0x08000000 + ((128-19)*2048))-8)
|
||||
END = (0x08000000 + ((128 - 19) * 2048)) - 8
|
||||
|
||||
ih = IntelHex(hex_file)
|
||||
segs = ih.segments()
|
||||
@ -92,6 +97,7 @@ def get_firmware_object(sk_name, hex_file):
|
||||
msg = {'firmware': fw, 'signature': sig}
|
||||
return msg
|
||||
|
||||
|
||||
class SoloBootloader:
|
||||
write = 0x40
|
||||
done = 0x41
|
||||
@ -109,8 +115,8 @@ class SoloBootloader:
|
||||
|
||||
TAG = b'\x8C\x27\x90\xf6'
|
||||
|
||||
class SoloClient():
|
||||
|
||||
class SoloClient:
|
||||
def __init__(self,):
|
||||
self.origin = 'https://example.org'
|
||||
self.exchange = self.exchange_hid
|
||||
@ -257,7 +263,9 @@ class SoloClient():
|
||||
of any updates.
|
||||
If you've started from a solo hacker, make you you've programmed a final/production build!
|
||||
"""
|
||||
ret = self.exchange(SoloBootloader.disable, 0, b'\xcd\xde\xba\xaa') # magic number
|
||||
ret = self.exchange(
|
||||
SoloBootloader.disable, 0, b'\xcd\xde\xba\xaa'
|
||||
) # magic number
|
||||
if ret[0] != CtapError.ERR.SUCCESS:
|
||||
print('Failed to disable bootloader')
|
||||
return False
|
||||
@ -265,7 +273,6 @@ class SoloClient():
|
||||
self.exchange(SoloBootloader.do_reboot)
|
||||
return True
|
||||
|
||||
|
||||
def program_file(self, name):
|
||||
|
||||
if name.lower().endswith('.json'):
|
||||
@ -314,10 +321,11 @@ class SoloClient():
|
||||
else:
|
||||
self.verify_flash(b'A' * 64)
|
||||
|
||||
|
||||
class DFU:
|
||||
class type:
|
||||
SEND = 0x21
|
||||
RECEIVE = 0xa1
|
||||
RECEIVE = 0xA1
|
||||
|
||||
class bmReq:
|
||||
DETACH = 0x00
|
||||
@ -339,7 +347,7 @@ class DFU:
|
||||
MANIFEST = 0x07
|
||||
MANIFEST_WAIT_RESET = 0x08
|
||||
UPLOAD_IDLE = 0x09
|
||||
ERROR = 0x0a
|
||||
ERROR = 0x0A
|
||||
|
||||
class status:
|
||||
def __init__(self, s):
|
||||
@ -348,23 +356,28 @@ class DFU:
|
||||
self.state = s[4]
|
||||
self.istring = s[5]
|
||||
|
||||
|
||||
# hot patch for windows libusb backend
|
||||
olddel = usb._objfinalizer._AutoFinalizedObjectBase.__del__
|
||||
|
||||
|
||||
def newdel(self):
|
||||
try:
|
||||
olddel(self)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
usb._objfinalizer._AutoFinalizedObjectBase.__del__ = newdel
|
||||
|
||||
|
||||
class DFUDevice:
|
||||
def __init__(self,):
|
||||
pass
|
||||
|
||||
|
||||
@staticmethod
|
||||
def addr2list(a):
|
||||
return [ a & 0xff, (a >> 8) & 0xff, (a >> 16) & 0xff, (a >> 24) & 0xff ]
|
||||
return [a & 0xFF, (a >> 8) & 0xFF, (a >> 16) & 0xFF, (a >> 24) & 0xFF]
|
||||
|
||||
@staticmethod
|
||||
def addr2block(addr, size):
|
||||
@ -391,8 +404,7 @@ class DFUDevice:
|
||||
self.dev = x
|
||||
break
|
||||
else:
|
||||
self.dev = usb.core.find(idVendor=0x0483, idProduct=0xDF11,)
|
||||
|
||||
self.dev = usb.core.find(idVendor=0x0483, idProduct=0xDF11)
|
||||
|
||||
if self.dev is None:
|
||||
raise RuntimeError('No ST DFU devices found.')
|
||||
@ -417,7 +429,9 @@ class DFUDevice:
|
||||
|
||||
def get_status(self,):
|
||||
# bmReqType, bmReq, wValue, wIndex, data/size
|
||||
s = self.dev.ctrl_transfer(DFU.type.RECEIVE, DFU.bmReq.GETSTATUS,0, self.intNum, 6)
|
||||
s = self.dev.ctrl_transfer(
|
||||
DFU.type.RECEIVE, DFU.bmReq.GETSTATUS, 0, self.intNum, 6
|
||||
)
|
||||
return DFU.status(s)
|
||||
|
||||
def state(self,):
|
||||
@ -425,14 +439,18 @@ class DFUDevice:
|
||||
|
||||
def clear_status(self,):
|
||||
# bmReqType, bmReq, wValue, wIndex, data/size
|
||||
s = self.dev.ctrl_transfer(DFU.type.SEND, DFU.bmReq.CLRSTATUS, 0, self.intNum, None)
|
||||
s = self.dev.ctrl_transfer(
|
||||
DFU.type.SEND, DFU.bmReq.CLRSTATUS, 0, self.intNum, None
|
||||
)
|
||||
|
||||
def upload(self, block, size):
|
||||
"""
|
||||
address is ((block – 2) × size) + 0x08000000
|
||||
"""
|
||||
# bmReqType, bmReq, wValue, wIndex, data/size
|
||||
return self.dev.ctrl_transfer(DFU.type.RECEIVE, DFU.bmReq.UPLOAD, block, self.intNum, size)
|
||||
return self.dev.ctrl_transfer(
|
||||
DFU.type.RECEIVE, DFU.bmReq.UPLOAD, block, self.intNum, size
|
||||
)
|
||||
|
||||
def set_addr(self, addr):
|
||||
# must get_status after to take effect
|
||||
@ -440,19 +458,21 @@ class DFUDevice:
|
||||
|
||||
def dnload(self, block, data):
|
||||
# bmReqType, bmReq, wValue, wIndex, data/size
|
||||
return self.dev.ctrl_transfer(DFU.type.SEND, DFU.bmReq.DNLOAD, block, self.intNum, data)
|
||||
return self.dev.ctrl_transfer(
|
||||
DFU.type.SEND, DFU.bmReq.DNLOAD, block, self.intNum, data
|
||||
)
|
||||
|
||||
def erase(self, a):
|
||||
d = [0x41, a & 0xff, (a >> 8) & 0xff, (a >> 16) & 0xff, (a >> 24) & 0xff]
|
||||
d = [0x41, a & 0xFF, (a >> 8) & 0xFF, (a >> 16) & 0xFF, (a >> 24) & 0xFF]
|
||||
return self.dnload(0x0, d)
|
||||
|
||||
def mass_erase(self):
|
||||
# self.set_addr(0x08000000)
|
||||
# self.block_on_state(DFU.state.DOWNLOAD_BUSY)
|
||||
# assert(DFU.state.DOWNLOAD_IDLE == self.state())
|
||||
self.dnload(0x0, [0x41,])
|
||||
self.dnload(0x0, [0x41])
|
||||
self.block_on_state(DFU.state.DOWNLOAD_BUSY)
|
||||
assert(DFU.state.DOWNLOAD_IDLE == self.state())
|
||||
assert DFU.state.DOWNLOAD_IDLE == self.state()
|
||||
|
||||
def write_page(self, addr, data):
|
||||
if self.state() not in (DFU.state.IDLE, DFU.state.DOWNLOAD_IDLE):
|
||||
@ -467,7 +487,7 @@ class DFUDevice:
|
||||
|
||||
self.dnload(addr, data)
|
||||
self.block_on_state(DFU.state.DOWNLOAD_BUSY)
|
||||
assert(DFU.state.DOWNLOAD_IDLE == self.state())
|
||||
assert DFU.state.DOWNLOAD_IDLE == self.state()
|
||||
|
||||
def read_mem(self, addr, size):
|
||||
addr = DFUDevice.addr2block(addr, size)
|
||||
@ -511,6 +531,7 @@ def attempt_to_find_device(p):
|
||||
time.sleep(0.2)
|
||||
return found
|
||||
|
||||
|
||||
def attempt_to_boot_bootloader(p):
|
||||
|
||||
try:
|
||||
@ -519,19 +540,25 @@ def attempt_to_boot_bootloader(p):
|
||||
pass
|
||||
except CtapError as e:
|
||||
if e.code == CtapError.ERR.INVALID_COMMAND:
|
||||
print('Solo appears to not be a solo hacker. Try holding down the button for 2 while you plug token in.')
|
||||
print(
|
||||
'Solo appears to not be a solo hacker. Try holding down the button for 2 while you plug token in.'
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise (e)
|
||||
print('Solo rebooted. Reconnecting...')
|
||||
time.sleep(.500)
|
||||
time.sleep(0.500)
|
||||
if not attempt_to_find_device(p):
|
||||
raise RuntimeError('Failed to reconnect!')
|
||||
|
||||
|
||||
def solo_main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--rng", action="store_true", help = 'Continuously dump random numbers generated from Solo.')
|
||||
parser.add_argument(
|
||||
"--rng",
|
||||
action="store_true",
|
||||
help='Continuously dump random numbers generated from Solo.',
|
||||
)
|
||||
parser.add_argument("--wink", action="store_true", help='HID Wink command.')
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -548,12 +575,14 @@ def solo_main():
|
||||
p.wink()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def asked_for_help():
|
||||
for i, v in enumerate(sys.argv):
|
||||
if v == '-h' or v == '--help':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def monitor_main():
|
||||
if asked_for_help() or len(sys.argv) != 2:
|
||||
print(
|
||||
@ -562,21 +591,24 @@ def monitor_main():
|
||||
usage: %s <serial-port> [-h]
|
||||
* <serial-port> will look like COM10 or /dev/ttyACM0 or something.
|
||||
* baud is 115200.
|
||||
""" % sys.argv[0])
|
||||
"""
|
||||
% sys.argv[0]
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
port = sys.argv[1]
|
||||
|
||||
ser = serial.Serial(port,115200,timeout=.05)
|
||||
ser = serial.Serial(port, 115200, timeout=0.05)
|
||||
|
||||
def reconnect():
|
||||
while(1):
|
||||
while 1:
|
||||
time.sleep(0.02)
|
||||
try:
|
||||
ser = serial.Serial(port,115200,timeout=.05)
|
||||
ser = serial.Serial(port, 115200, timeout=0.05)
|
||||
return ser
|
||||
except serial.SerialException:
|
||||
pass
|
||||
|
||||
while 1:
|
||||
try:
|
||||
d = ser.read(1)
|
||||
@ -587,6 +619,7 @@ def monitor_main():
|
||||
sys.stdout.buffer.write(d)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def genkey_main():
|
||||
from ecdsa import SigningKey, NIST256p
|
||||
from ecdsa.util import randrange_from_seed__trytryagain
|
||||
@ -600,7 +633,9 @@ def genkey_main():
|
||||
* Public key must be copied into correct source location in solo bootloader
|
||||
* The private key can be used for signing updates.
|
||||
* You may optionally supply a file to seed the RNG for key generating.
|
||||
""" % sys.argv[0])
|
||||
"""
|
||||
% sys.argv[0]
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
@ -627,10 +662,13 @@ def genkey_main():
|
||||
print('"\\x' + '\\x'.join(['%02x' % c for c in vk.to_string()]) + '"')
|
||||
print()
|
||||
|
||||
|
||||
def sign_main():
|
||||
|
||||
if asked_for_help() or len(sys.argv) != 4:
|
||||
print('Signs a firmware hex file, outputs a .json file that can be used for signed update.')
|
||||
print(
|
||||
'Signs a firmware hex file, outputs a .json file that can be used for signed update.'
|
||||
)
|
||||
print('usage: %s <signing-key.pem> <app.hex> <output.json> [-h]' % sys.argv[0])
|
||||
print()
|
||||
sys.exit(1)
|
||||
@ -640,6 +678,7 @@ def sign_main():
|
||||
wfile.write(json.dumps(msg).encode())
|
||||
wfile.close()
|
||||
|
||||
|
||||
def use_dfu(args):
|
||||
fw = args.__dict__['[firmware]']
|
||||
|
||||
@ -685,7 +724,10 @@ def use_dfu(args):
|
||||
total += chunk
|
||||
progress = total / float(size) * 100
|
||||
|
||||
sys.stdout.write('downloading %.2f%% %08x - %08x ... \r' % (progress,i,i+page))
|
||||
sys.stdout.write(
|
||||
'downloading %.2f%% %08x - %08x ... \r'
|
||||
% (progress, i, i + page)
|
||||
)
|
||||
# time.sleep(0.100)
|
||||
|
||||
# print('done')
|
||||
@ -697,43 +739,88 @@ def use_dfu(args):
|
||||
progress = 0
|
||||
for start, end in ih.segments():
|
||||
for i in range(start, end, chunk):
|
||||
data1 = (dfu.read_mem(i,2048))
|
||||
data1 = dfu.read_mem(i, 2048)
|
||||
data2 = ih.tobinarray(start=i, size=chunk)
|
||||
total += chunk
|
||||
progress = total / float(size) * 100
|
||||
sys.stdout.write('reading %.2f%% %08x - %08x ... \r' % (progress,i,i+page))
|
||||
sys.stdout.write(
|
||||
'reading %.2f%% %08x - %08x ... \r'
|
||||
% (progress, i, i + page)
|
||||
)
|
||||
if (end - start) == chunk:
|
||||
assert(data1 == data2)
|
||||
assert data1 == data2
|
||||
print()
|
||||
print('firmware readback verified.')
|
||||
if args.detach:
|
||||
dfu.detach()
|
||||
|
||||
|
||||
|
||||
def programmer_main():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("[firmware]", nargs='?', default='', help = 'firmware file. Either a JSON or hex file. JSON file contains signature while hex does not.')
|
||||
parser.add_argument("--use-hid", action="store_true", help = 'Programs using custom HID command (default). Quicker than using U2F authenticate which is what a browser has to use.')
|
||||
parser.add_argument("--use-u2f", action="store_true", help = 'Programs using U2F authenticate. This is what a web application will use.')
|
||||
parser.add_argument("--no-reset", action="store_true", help = 'Don\'t reset after writing firmware. Stay in bootloader mode.')
|
||||
parser.add_argument("--reset-only", action="store_true", help = 'Don\'t write anything, try to boot without a signature.')
|
||||
parser.add_argument("--reboot", action="store_true", help = 'Tell bootloader to reboot.')
|
||||
parser.add_argument("--enter-bootloader", action="store_true", help = 'Don\'t write anything, try to enter bootloader. Typically only supported by Solo Hacker builds.')
|
||||
parser.add_argument("--st-dfu", action="store_true", help = 'Don\'t write anything, try to enter ST DFU. Warning, you could brick your Solo if you overwrite everything. You should reprogram the option bytes just to be safe (boot to Solo bootloader first, then run this command).')
|
||||
parser.add_argument("--disable", action="store_true", help = 'Disable the Solo bootloader. Cannot be undone. No future updates can be applied.')
|
||||
parser.add_argument("--detach", action="store_true", help = 'Detach from ST DFU and boot from main flash. Must be in DFU mode.')
|
||||
parser.add_argument("--dfu-serial", default='', help = 'Specify a serial number for a specific DFU device to connect to.')
|
||||
parser.add_argument("--use-dfu", action="store_true", help = 'Boot to ST-DFU before continuing.')
|
||||
parser.add_argument(
|
||||
"[firmware]",
|
||||
nargs='?',
|
||||
default='',
|
||||
help='firmware file. Either a JSON or hex file. JSON file contains signature while hex does not.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-hid",
|
||||
action="store_true",
|
||||
help='Programs using custom HID command (default). Quicker than using U2F authenticate which is what a browser has to use.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-u2f",
|
||||
action="store_true",
|
||||
help='Programs using U2F authenticate. This is what a web application will use.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-reset",
|
||||
action="store_true",
|
||||
help='Don\'t reset after writing firmware. Stay in bootloader mode.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reset-only",
|
||||
action="store_true",
|
||||
help='Don\'t write anything, try to boot without a signature.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reboot", action="store_true", help='Tell bootloader to reboot.'
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enter-bootloader",
|
||||
action="store_true",
|
||||
help='Don\'t write anything, try to enter bootloader. Typically only supported by Solo Hacker builds.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--st-dfu",
|
||||
action="store_true",
|
||||
help='Don\'t write anything, try to enter ST DFU. Warning, you could brick your Solo if you overwrite everything. You should reprogram the option bytes just to be safe (boot to Solo bootloader first, then run this command).',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable",
|
||||
action="store_true",
|
||||
help='Disable the Solo bootloader. Cannot be undone. No future updates can be applied.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--detach",
|
||||
action="store_true",
|
||||
help='Detach from ST DFU and boot from main flash. Must be in DFU mode.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dfu-serial",
|
||||
default='',
|
||||
help='Specify a serial number for a specific DFU device to connect to.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-dfu", action="store_true", help='Boot to ST-DFU before continuing.'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
fw = args.__dict__['[firmware]']
|
||||
|
||||
p = SoloClient()
|
||||
|
||||
|
||||
|
||||
try:
|
||||
p.find_device()
|
||||
if args.use_dfu:
|
||||
@ -780,7 +867,6 @@ def programmer_main():
|
||||
p.disable_solo_bootloader()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if fw == '' and not args.reset_only:
|
||||
print('Need to supply firmware filename, or see help for more options.')
|
||||
parser.print_help()
|
||||
@ -806,7 +892,9 @@ def programmer_main():
|
||||
|
||||
def main_mergehex():
|
||||
if len(sys.argv) < 3:
|
||||
print('usage: %s <file1.hex> <file2.hex> [...] [-s <secret_attestation_key>] <output.hex>')
|
||||
print(
|
||||
'usage: %s <file1.hex> <file2.hex> [...] [-s <secret_attestation_key>] <output.hex>'
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
def flash_addr(num):
|
||||
@ -815,7 +903,9 @@ def main_mergehex():
|
||||
args = sys.argv[:]
|
||||
|
||||
# generic / hacker attestation key
|
||||
secret_attestation_key = "1b2626ecc8f69b0f69e34fb236d76466ba12ac16c3ab5750ba064e8b90e02448"
|
||||
secret_attestation_key = (
|
||||
"1b2626ecc8f69b0f69e34fb236d76466ba12ac16c3ab5750ba064e8b90e02448"
|
||||
)
|
||||
|
||||
# user supplied, optional
|
||||
for i, x in enumerate(args):
|
||||
@ -824,12 +914,11 @@ def main_mergehex():
|
||||
args = args[:i] + args[i + 2 :]
|
||||
break
|
||||
|
||||
|
||||
# TODO put definitions somewhere else
|
||||
PAGES = 128
|
||||
APPLICATION_END_PAGE = PAGES - 19
|
||||
AUTH_WORD_ADDR = (flash_addr(APPLICATION_END_PAGE)-8)
|
||||
ATTEST_ADDR = (flash_addr(PAGES - 15))
|
||||
AUTH_WORD_ADDR = flash_addr(APPLICATION_END_PAGE) - 8
|
||||
ATTEST_ADDR = flash_addr(PAGES - 15)
|
||||
|
||||
first = IntelHex(args[1])
|
||||
for i in range(2, len(args) - 1):
|
||||
@ -849,21 +938,20 @@ def main_mergehex():
|
||||
first[AUTH_WORD_ADDR + 2] = 0
|
||||
first[AUTH_WORD_ADDR + 3] = 0
|
||||
|
||||
first[AUTH_WORD_ADDR+4] = 0xff
|
||||
first[AUTH_WORD_ADDR+5] = 0xff
|
||||
first[AUTH_WORD_ADDR+6] = 0xff
|
||||
first[AUTH_WORD_ADDR+7] = 0xff
|
||||
|
||||
first[AUTH_WORD_ADDR + 4] = 0xFF
|
||||
first[AUTH_WORD_ADDR + 5] = 0xFF
|
||||
first[AUTH_WORD_ADDR + 6] = 0xFF
|
||||
first[AUTH_WORD_ADDR + 7] = 0xFF
|
||||
|
||||
if secret_attestation_key is not None:
|
||||
key = unhexlify(secret_attestation_key)
|
||||
|
||||
|
||||
for i, x in enumerate(key):
|
||||
first[ATTEST_ADDR + i] = x
|
||||
|
||||
first.tofile(args[len(args) - 1], format='hex')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if len(sys.argv) < 2 or (len(sys.argv) == 2 and asked_for_help()):
|
||||
@ -884,10 +972,12 @@ Examples:
|
||||
{0} sign <key.pem> <firmware.hex> <output.json>
|
||||
{0} genkey <output-pem-file> [rng-seed-file]
|
||||
{0} mergehex bootloader.hex solo.hex combined.hex
|
||||
""".format(sys.argv[0]))
|
||||
""".format(
|
||||
sys.argv[0]
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
c = sys.argv[1]
|
||||
sys.argv = sys.argv[:1] + sys.argv[2:]
|
||||
sys.argv[0] = sys.argv[0] + ' ' + c
|
||||
|
Loading…
x
Reference in New Issue
Block a user