From b2c78ca7c096eeac3dc7d4ae1ee652864a37f411 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 14:24:34 +0100 Subject: [PATCH 01/15] Start some cleanup - add an editorconfig - fix requirements.txt and the UTF-8 issue (lost in pull/47) - two clean environments - make black usable - two test targets (wink2/wink3) --- .editorconfig | 19 +++++++++++++++++++ .gitignore | 3 ++- Makefile | 38 +++++++++++++++++++++----------------- tools/requirements.txt | 2 +- tools/solotool.py | 1 + 5 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0d06da3 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore index e0bddba..4e3c5c5 100644 --- a/.gitignore +++ b/.gitignore @@ -74,7 +74,8 @@ tools/python-fido2/* *.key site/ _site/ -venv/ +env2/ +env3/ .project .tags* targets/*/docs/ diff --git a/Makefile b/Makefile index 4b9b8f2..c6398db 100644 --- a/Makefile +++ b/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 @@ -29,7 +29,7 @@ INCLUDES = -I./tinycbor/src -I./crypto/sha256 -I./crypto/micro-ecc/ -Icrypto/tin CFLAGS += $(INCLUDES) # for crypto/tiny-AES-c -CFLAGS += -DAES256=1 -DAPP_CONFIG=\"app.h\" +CFLAGS += -DAES256=1 -DAPP_CONFIG=\"app.h\" name = main @@ -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 env2 + env2/bin/pip install -r tools/requirements.txt -# python virtualenv +env3: + python3 -m venv env3 + env3/bin/pip install -r tools/requirements.txt + env3/bin/pip install 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 -S 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 -- .) ;\ diff --git a/tools/requirements.txt b/tools/requirements.txt index 2d1726d..7b71a23 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,5 +1,5 @@ ecdsa intelhex pyserial -python-fido2 +fido2 pyusb diff --git a/tools/solotool.py b/tools/solotool.py index cd95195..3823c56 100644 --- a/tools/solotool.py +++ b/tools/solotool.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. # From 6a5449b8cbb3e60c2481247517aae72a3defe2df Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 14:27:21 +0100 Subject: [PATCH 02/15] Pass tools/ through black --- tools/convert_log_to_c.py | 18 +- tools/ctap_test.py | 466 ++++++++++++++++++++----------------- tools/gencert/cbytes.py | 25 +- tools/gencert/dump_pem.py | 16 +- tools/gencert/print_x_y.py | 16 +- tools/http2udb.py | 93 ++++---- tools/nfcmon.py | 18 +- tools/solotool.py | 436 ++++++++++++++++++++-------------- 8 files changed, 614 insertions(+), 474 deletions(-) diff --git a/tools/convert_log_to_c.py b/tools/convert_log_to_c.py index a24169c..5b54af4 100644 --- a/tools/convert_log_to_c.py +++ b/tools/convert_log_to_c.py @@ -1,21 +1,21 @@ # # Copyright (C) 2018 SoloKeys, Inc. -# +# # This file is part of Solo. -# +# # Solo is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # Solo is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with Solo. If not, see -# +# # This code is available under licenses for commercial use. # Please contact SoloKeys for more information. # @@ -23,7 +23,7 @@ import sys from sys import argv if len(argv) != 2: - print("usage: %s " % argv[0]); + print("usage: %s " % argv[0]) sys.exit(1) log = open(argv[1]).readlines() @@ -34,13 +34,13 @@ for x in log: parse = [] for i in x.split(' '): try: - n = int(i,16) + n = int(i, 16) parse.append(n) except: pass if len(parse) == 0: continue - assert(len(parse) == 64) + assert len(parse) == 64 nums.append(parse) hexlines = [] @@ -52,4 +52,4 @@ for l in nums: hexlines.append(s) for x in hexlines: - print('"'+x+'"') + print('"' + x + '"') diff --git a/tools/ctap_test.py b/tools/ctap_test.py index 58de325..d137901 100644 --- a/tools/ctap_test.py +++ b/tools/ctap_test.py @@ -1,21 +1,21 @@ # # Copyright (C) 2018 SoloKeys, Inc. -# +# # This file is part of Solo. -# +# # Solo is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # Solo is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with Solo. If not, see -# +# # This code is available under licenses for commercial use. # Please contact SoloKeys for more information. # @@ -31,15 +31,15 @@ from fido2.ctap1 import CTAP1 from fido2.ctap2 import * from fido2.cose import * from fido2.utils import Timeout -import sys,os,time +import sys, os, time from random import randint from binascii import hexlify -import array,struct,socket +import array, struct, socket # Set up a FIDO 2 client using the origin https://example.com -def ForceU2F(client,device): +def ForceU2F(client, device): client.ctap = CTAP1(device) client.pin_protocol = None client._do_make_credential = client._ctap1_make_credential @@ -47,7 +47,7 @@ def ForceU2F(client,device): class Packet(object): - def __init__(self,data): + def __init__(self, data): l = len(data) self.data = data @@ -55,16 +55,17 @@ class Packet(object): return self.data @staticmethod - def FromWireFormat(pkt_size,data): + def FromWireFormat(pkt_size, data): return Packet(data) -class Tester(): + +class Tester: def __init__(self,): self.origin = 'https://examplo.org' self.host = 'examplo.org' def find_device(self,): - print (list(CtapHidDevice.list_devices())) + print(list(CtapHidDevice.list_devices())) dev = next(CtapHidDevice.list_devices(), None) if not dev: raise RuntimeError('No FIDO device found') @@ -73,15 +74,15 @@ class Tester(): self.ctap = self.client.ctap2 # consume timeout error - #cmd,resp = self.recv_raw() + # cmd,resp = self.recv_raw() def send_data(self, cmd, data): if type(data) != type(b''): data = struct.pack('%dB' % len(data), *[ord(x) for x in data]) with Timeout(1.0) as event: - return self.dev.call(cmd, data,event) + return self.dev.call(cmd, data, event) - def send_raw(self, data, cid = None): + def send_raw(self, data, cid=None): if cid is None: cid = self.dev._dev.cid elif type(cid) != type(b''): @@ -91,28 +92,28 @@ class Tester(): data = cid + data l = len(data) if l != 64: - pad = '\x00' * (64-l) + pad = '\x00' * (64 - l) 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,): return self.dev._dev.cid - def set_cid(self,cid): + def set_cid(self, cid): if type(cid) not in [type(b''), type(bytearray())]: cid = struct.pack('%dB' % len(cid), *[ord(x) for x in cid]) self.dev._dev.cid = cid def recv_raw(self,): with Timeout(1.0) as t: - cmd,payload = self.dev._dev.InternalRecv() + cmd, payload = self.dev._dev.InternalRecv() return cmd, payload - def check_error(self,data,err=None): - assert(len(data) == 1) + def check_error(self, data, err=None): + assert len(data) == 1 if err is None: if data[0] != 0: raise CtapError(data[0]) @@ -127,37 +128,35 @@ class Tester(): r = self.send_data(CTAPHID.PING, pingdata) t2 = time.time() * 1000 delt = t2 - t1 - #if (delt < 140 ): - #raise RuntimeError('Fob is too fast (%d ms)' % delt) - if (delt > 555 * (amt/1000)): + # if (delt < 140 ): + # raise RuntimeError('Fob is too fast (%d ms)' % delt) + 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: print('7609 byte Ping failed:', e) raise RuntimeError('ping failed') print('PASS: 7609 byte ping') - #sys.flush(sys.sto) + # sys.flush(sys.sto) sys.stdout.flush() - - def test_hid(self,check_timeouts = False): + def test_hid(self, check_timeouts=False): if check_timeouts: print('Test idle') try: - cmd,resp = self.recv_raw() + cmd, resp = self.recv_raw() except socket.timeout: print('Pass: Idle') 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) @@ -169,25 +168,25 @@ class Tester(): try: r = self.send_data(CTAPHID.WINK, '') print(hexlify(r)) - #assert(len(r) == 0) + # assert(len(r) == 0) except CtapError as e: print('wink failed:', e) raise RuntimeError('wink failed') print('PASS: wink') - #try: - #r = self.send_data(CTAPHID.WINK, 'we9gofrei8g') - #raise RuntimeError('Wink is not supposed to have payload') - #except CtapError as e: - #assert(e.code == CtapError.ERR.INVALID_LENGTH) - #print('PASS: malformed wink') + # try: + # r = self.send_data(CTAPHID.WINK, 'we9gofrei8g') + # raise RuntimeError('Wink is not supposed to have payload') + # except CtapError as e: + # assert(e.code == CtapError.ERR.INVALID_LENGTH) + # print('PASS: malformed wink') try: r = self.send_data(CTAPHID.CBOR, '') 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 +195,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,28 +208,27 @@ 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() + cmd, resp = self.recv_raw() self.check_error(resp, CtapError.ERR.INVALID_LENGTH) print('PASS: invalid length') - r = self.send_data(CTAPHID.PING, '\x44'*200) + r = self.send_data(CTAPHID.PING, '\x44' * 200) print('Sending packets that skip a sequence number.') self.send_raw('\x81\x04\x90') self.send_raw('\x00') self.send_raw('\x01') # skip 2 self.send_raw('\x03') - cmd,resp = self.recv_raw() + cmd, resp = self.recv_raw() self.check_error(resp, CtapError.ERR.INVALID_SEQ) if check_timeouts: - cmd,resp = self.recv_raw() - assert(cmd == 0xbf) # timeout + cmd, resp = self.recv_raw() + assert cmd == 0xBF # timeout print('PASS: invalid sequence') print('Resync and send ping') @@ -238,7 +236,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 +259,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) + cmd, r = self.recv_raw() # init response + assert cmd == 0x86 self.set_cid(oldcid) if check_timeouts: - #print('wait for timeout') - cmd,r = self.recv_raw() # timeout response - assert(cmd == 0xbf) + # print('wait for timeout') + cmd, r = self.recv_raw() # timeout response + assert cmd == 0xBF print('PASS: resync and timeout') @@ -279,12 +279,12 @@ class Tester(): self.send_raw('\x81\x04\x00') self.send_raw('\x00') self.send_raw('\x01') - cmd,r = self.recv_raw() # timeout response + 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') @@ -292,10 +292,10 @@ class Tester(): self.send_raw('\x81\x04\x00') self.send_raw('\x00') 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) + 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 print('PASS: Test not cont') if check_timeouts: @@ -303,7 +303,7 @@ class Tester(): self.send_data(CTAPHID.INIT, '\x11\x22\x33\x44\x55\x66\x77\x88') self.send_raw('\x01\x10\x00') try: - cmd,r = self.recv_raw() # timeout response + cmd, r = self.recv_raw() # timeout response except socket.timeout: pass print('PASS: random cont') @@ -316,16 +316,16 @@ class Tester(): self.send_raw('\x81\x04\x00') self.set_cid(newcid) self.send_raw('\x81\x04\x00') - cmd,r = self.recv_raw() # busy response + 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) + cmd, r = self.recv_raw() # timeout response + assert cmd == 0xBF + assert r[0] == CtapError.ERR.TIMEOUT print('PASS: busy') print('Check busy interleaved') @@ -335,55 +335,59 @@ class Tester(): self.send_data(CTAPHID.INIT, '\x11\x22\x33\x44\x55\x66\x77\x88') self.set_cid(cid1) self.send_data(CTAPHID.INIT, '\x11\x22\x33\x44\x55\x66\x77\x88') - self.send_raw('\x81\x00\x63') # echo 99 bytes first channel + self.send_raw('\x81\x00\x63') # echo 99 bytes first channel self.set_cid(cid2) # send ping on 2nd channel self.send_raw('\x81\x00\x63') self.send_raw('\x00') - cmd,r = self.recv_raw() # busy response + cmd, r = self.recv_raw() # busy response - self.set_cid(cid1) # finish 1st channel ping + self.set_cid(cid1) # finish 1st channel ping self.send_raw('\x00') 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) + cmd, r = self.recv_raw() # ping response + 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) + cmd, r = self.recv_raw() # timeout + assert cmd == 0xBF + assert r[0] == CtapError.ERR.TIMEOUT print('PASS: busy interleaved') if check_timeouts: print('Test idle, wait for timeout') sys.stdout.flush() try: - cmd,resp = self.recv_raw() + cmd, resp = self.recv_raw() except socket.timeout: print('Pass: Idle') 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') - cmd,r = self.recv_raw() # timeout - assert(cmd == 0xbf) - assert(r[0] == CtapError.ERR.INVALID_CHANNEL) + 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 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') - cmd,r = self.recv_raw() # timeout - assert(cmd == 0xbf) - assert(r[0] == CtapError.ERR.INVALID_CHANNEL) + 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 print('Pass: cid broadcast') def test_u2f(self,): @@ -392,34 +396,38 @@ class Tester(): def test_fido2_simple(self, pin_token=None): creds = [] exclude_list = [] - rp = {'id': self.host, 'name': 'ExaRP'} + rp = {'id': self.host, 'name': 'ExaRP'} user = {'id': b'usee_od', 'name': 'AB User'} challenge = 'Y2hhbGxlbmdl' PIN = pin_token - fake_id1 = array.array('B',[randint(0,255) for i in range(0,150)]).tobytes() - fake_id2 = array.array('B',[randint(0,255) for i in range(0,73)]).tobytes() + fake_id1 = array.array('B', [randint(0, 255) for i in range(0, 150)]).tobytes() + fake_id2 = array.array('B', [randint(0, 255) for i in range(0, 73)]).tobytes() exclude_list.append({'id': fake_id1, 'type': 'public-key'}) exclude_list.append({'id': fake_id2, 'type': 'public-key'}) 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)) + print('Register valid (%d ms)' % (t2 - t1)) cred = attest.auth_data.credential_data creds.append(cred) - allow_list = [{'id':creds[0].credential_id, 'type': 'public-key'}] + 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('Assertion valid (%d ms)' % (t2-t1)) + print('Assertion valid (%d ms)' % (t2 - t1)) def test_fido2_brute_force(self): creds = [] @@ -432,46 +440,52 @@ class Tester(): self.ctap.reset() - for i in range(0,2048**2): + for i in range(0, 2048 ** 2): creds = [] - challenge = ''.join([abc[randint(0,len(abc)-1)] for x in range(0,32)]) + 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'}) - #for i in range(0,2048**2): - for i in range(0,1): + # 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) - print('Register valid (%d ms)' % (t2-t1)) + print('Register valid (%d ms)' % (t2 - t1)) sys.stdout.flush() cred = attest.auth_data.credential_data creds.append(cred) - #for i in range(0,2048**2): - for i in range(0,1): - allow_list = [{'id':creds[0].credential_id, 'type': 'public-key'}] + # for i in range(0,2048**2): + 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) - print('Assertion valid (%d ms)' % (t2-t1)) + print('Assertion valid (%d ms)' % (t2 - t1)) sys.stdout.flush() - - def test_fido2(self): - def test(self,pincode=None): + def test(self, pincode=None): creds = [] exclude_list = [] rp = {'id': self.host, 'name': 'ExaRP'} @@ -479,16 +493,22 @@ 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'}) # 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 = []) + for i in range(0, 3): + 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 +518,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,37 +539,44 @@ 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) + allow_list = [{'id': x.credential_id, 'type': 'public-key'}] + 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): + for ass, cred in zip(assertions, creds): i += 1 ass.verify(client_data.hash, cred.public_key) @@ -571,34 +602,34 @@ 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') PIN2 = PIN + '_pin2' - self.client.pin_protocol.change_pin(PIN,PIN2) + self.client.pin_protocol.change_pin(PIN, PIN2) PIN = PIN2 print('PASS') print('Change pin code using wrong pin') try: - self.client.pin_protocol.change_pin(PIN.replace('a','b'),'1234') + 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) @@ -610,158 +641,171 @@ class Tester(): print('Warning, reset failed: ', e) print('PASS') - def test_rk(self, ): + def test_rk(self,): creds = [] 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') self.ctap.reset() - #if PIN: self.client.pin_protocol.set_pin(PIN) + # if PIN: self.client.pin_protocol.set_pin(PIN) 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) - print('Register valid (%d ms)' % (t2-t1)) + print('Register valid (%d ms)' % (t2 - t1)) 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('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)): + 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)) + 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): - x.verify(client_data.hash,y.public_key) - - print('Assertion(s) valid (%d ms)' % (t2-t1)) + for x, y in zip(assertions, creds): + x.verify(client_data.hash, y.public_key) + 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)) - + 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) - for x,y in zip(assertions, creds): - x.verify(client_data.hash,y.public_key) - - print('Assertion(s) valid (%d ms)' % (t2-t1)) + 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()): - print('dev',dev) + 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) - print('versions: ',inf.versions) - print('aaguid: ',inf.aaguid) - print('rk: ',inf.options['rk']) - print('clientPin: ',inf.options['clientPin']) - print('max_message_size: ',inf.max_msg_size) + # print (inf) + print('versions: ', inf.versions) + print('aaguid: ', inf.aaguid) + print('rk: ', inf.options['rk']) + print('clientPin: ', inf.options['clientPin']) + print('max_message_size: ', inf.max_msg_size) - #rp = {'id': 'SelectDevice', 'name': 'SelectDevice'} + # rp = {'id': 'SelectDevice', 'name': 'SelectDevice'} rp = {'id': RPID, 'name': 'ExaRP'} user = {'id': os.urandom(10), 'name': 'SelectDevice'} user = {'id': b'21first one', 'name': 'single User'} 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 = [{'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] - print('Register valid (%d ms)' % (t2-t1)) + print('Register valid (%d ms)' % (t2 - t1)) 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('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)) - #print('count:', hex(attest.auth_data.counter)) - print('flags MC:',attest.auth_data) - print('flags GA:',assertions[0].auth_data) - #print('cred_id:',attest.auth_data.credential_data.credential_id) - #print('pubkey:',attest.auth_data.credential_data.public_key) - #print('aaguid:',attest.auth_data.credential_data.aaguid) + # print('fmt:',attest.fmt) + # print('rp_id_hash',attest.auth_data.rp_id_hash) + # print('flags:', hex(attest.auth_data.flags)) + # print('count:', hex(attest.auth_data.counter)) + print('flags MC:', attest.auth_data) + print('flags GA:', assertions[0].auth_data) + # print('cred_id:',attest.auth_data.credential_data.credential_id) + # print('pubkey:',attest.auth_data.credential_data.public_key) + # print('aaguid:',attest.auth_data.credential_data.aaguid) # print('cred data:',attest.auth_data.credential_data) # print('auth_data:',attest.auth_data) - #print('auth_data:',attest.auth_data) - #print('alg:',attest.att_statement['alg']) - #print('sig:',attest.att_statement['sig']) - #print('x5c:',attest.att_statement['x5c']) - #print('data:',data) + # print('auth_data:',attest.auth_data) + # print('alg:',attest.att_statement['alg']) + # print('sig:',attest.att_statement['sig']) + # print('x5c:',attest.att_statement['x5c']) + # print('data:',data) print('assertion:', assertions[0]) print('clientData:', client_data) print() - #break - + # break def test_find_brute_force(): @@ -771,7 +815,7 @@ def test_find_brute_force(): t = Tester() t.find_device() t2 = time.time() * 1000 - print('connected %d (%d ms)' % (i, t2-t1)) + print('connected %d (%d ms)' % (i, t2 - t1)) i += 1 time.sleep(0.01) @@ -782,8 +826,8 @@ if __name__ == '__main__': # t.test_hid() # t.test_long_ping() t.test_fido2() - #t.test_rk() - #t.test_responses() + # t.test_rk() + # t.test_responses() # test_find_brute_force() - #t.test_fido2_simple() - #t.test_fido2_brute_force() + # t.test_fido2_simple() + # t.test_fido2_brute_force() diff --git a/tools/gencert/cbytes.py b/tools/gencert/cbytes.py index 1cbb28f..9c1f507 100644 --- a/tools/gencert/cbytes.py +++ b/tools/gencert/cbytes.py @@ -1,36 +1,37 @@ #!/usr/bin/env python # # Copyright (C) 2018 SoloKeys, Inc. -# +# # This file is part of Solo. -# +# # Solo is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # Solo is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with Solo. If not, see -# +# # This code is available under licenses for commercial use. # Please contact SoloKeys for more information. # from __future__ import print_function import base64 + """ cbytes.py Output a c file with the DER certificate. Read der file as input """ -import sys,fileinput,binascii +import sys, fileinput, binascii -if len(sys.argv) not in [2,3]: +if len(sys.argv) not in [2, 3]: print('usage: %s [-s]' % sys.argv[0]) print(' -s: just output c string (for general use)') sys.exit(1) @@ -39,17 +40,17 @@ buf = None try: buf = bytearray(open(sys.argv[1], 'rb').read()) except: - n = sys.argv[1].replace('\n','') - n = sys.argv[1].replace('\r','') + n = sys.argv[1].replace('\n', '') + n = sys.argv[1].replace('\r', '') buf = bytearray(binascii.unhexlify(n)) c_str = '' size = len(buf) -a = ''.join(map(lambda c:'\\x%02x'%c, 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") +for i in range(0, len(a), 80): + c_str += "\"" + a[i : i + 80] + "\"\n" if '-s' in sys.argv: print(c_str) diff --git a/tools/gencert/dump_pem.py b/tools/gencert/dump_pem.py index b7d46f1..ec78ca6 100644 --- a/tools/gencert/dump_pem.py +++ b/tools/gencert/dump_pem.py @@ -1,27 +1,28 @@ #!/usr/bin/env python # # Copyright (C) 2018 SoloKeys, Inc. -# +# # This file is part of Solo. -# +# # Solo is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # Solo is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with Solo. If not, see -# +# # This code is available under licenses for commercial use. # Please contact SoloKeys for more information. # from __future__ import print_function -import sys,fileinput,binascii +import sys, fileinput, binascii + try: import ecdsa except: @@ -45,7 +46,6 @@ cstr = '' it = iter(hstr) for d1 in it: d2 = next(it) - cstr += '\\x'+d1+d2 + cstr += '\\x' + d1 + d2 print('"%s"' % cstr) - diff --git a/tools/gencert/print_x_y.py b/tools/gencert/print_x_y.py index 0feb565..b527264 100644 --- a/tools/gencert/print_x_y.py +++ b/tools/gencert/print_x_y.py @@ -1,21 +1,21 @@ # # Copyright (C) 2018 SoloKeys, Inc. -# +# # This file is part of Solo. -# +# # Solo is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # Solo is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with Solo. If not, see -# +# # This code is available under licenses for commercial use. # Please contact SoloKeys for more information. # @@ -29,9 +29,7 @@ print('Private key in various formats:') print() print([c for c in sk.to_string()]) print() -print(''.join(['%02x'%c for c in sk.to_string()])) +print(''.join(['%02x' % c for c in sk.to_string()])) print() -print('"\\x' + '\\x'.join(['%02x'%c for c in sk.to_string()]) + '"') +print('"\\x' + '\\x'.join(['%02x' % c for c in sk.to_string()]) + '"') print() - - diff --git a/tools/http2udb.py b/tools/http2udb.py index 41503e3..ecc5377 100644 --- a/tools/http2udb.py +++ b/tools/http2udb.py @@ -1,27 +1,27 @@ #!/usr/bin/python # # Copyright (C) 2018 SoloKeys, Inc. -# +# # This file is part of Solo. -# +# # Solo is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # Solo is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with Solo. If not, see -# +# # This code is available under licenses for commercial use. # Please contact SoloKeys for more information. # from __future__ import print_function, absolute_import, unicode_literals -from http.server import BaseHTTPRequestHandler,HTTPServer +from http.server import BaseHTTPRequestHandler, HTTPServer from fido2.hid import CtapHidDevice, CTAPHID from fido2.client import Fido2Client, ClientError @@ -35,7 +35,7 @@ from intelhex import IntelHex from ecdsa import SigningKey, NIST256p -import socket,json,base64,ssl,array,binascii +import socket, json, base64, ssl, array, binascii from sign_firmware import * @@ -44,7 +44,8 @@ udpport = 8111 HEX_FILE = '../efm32/GNU ARM v7.2.1 - Debug/EFM32.hex' -def ForceU2F(client,device): + +def ForceU2F(client, device): client.ctap = CTAP1(device) client.pin_protocol = None client._do_make_credential = client._ctap1_make_credential @@ -64,39 +65,39 @@ if __name__ == '__main__': print(e) - - def write(data): msg = from_websafe(data) msg = base64.b64decode(msg) - chal = b'A'*32 - appid = b'A'*32 - #print (msg) - #print (msg.decode()) - #print (str(msg)) - #msg = msg.decode('ascii') - #print('ascii:',repr(msg)) - #print('ascii:',(type(msg))) - #print(msg + chal) + chal = b'A' * 32 + appid = b'A' * 32 + # print (msg) + # print (msg.decode()) + # print (str(msg)) + # msg = msg.decode('ascii') + # print('ascii:',repr(msg)) + # print('ascii:',(type(msg))) + # print(msg + chal) - #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,) + # 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) print(s) - #sock.sendto(msg, ('127.0.0.1', udpport)) + # sock.sendto(msg, ('127.0.0.1', udpport)) + def read(): - #msg = [0]*64 + # msg = [0]*64 pkt, _ = sock.recvfrom(1000) - #for i,v in enumerate(pkt): - #msg[i] = ord(v) + # for i,v in enumerate(pkt): + # msg[i] = ord(v) msg = base64.b64encode(pkt) msg = to_websafe(pkt) return msg + class UDPBridge(BaseHTTPRequestHandler): - def end_headers (self): + def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') BaseHTTPRequestHandler.end_headers(self) @@ -109,43 +110,50 @@ class UDPBridge(BaseHTTPRequestHandler): msg = from_websafe(data) msg = base64.b64decode(msg) 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 + 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}) + data = json.dumps({'data': data}) data = data.encode('ascii') self.send_response(200) - self.send_header('Content-type','text/json') + self.send_header('Content-type', 'text/json') self.end_headers() self.wfile.write(data) - def do_GET(self): self.send_response(200) - self.send_header('Content-type','text/json') + self.send_header('Content-type', 'text/json') - msg = get_firmware_object("signing_key.pem",HEX_FILE) + msg = get_firmware_object("signing_key.pem", HEX_FILE) self.end_headers() self.wfile.write(json.dumps(msg).encode()) + try: server = HTTPServer(('', httpport), UDPBridge) - print('Started httpserver on port ' , httpport) + print('Started httpserver on port ', httpport) - server.socket = ssl.wrap_socket (server.socket, - keyfile="../web/localhost.key", - certfile='../web/localhost.crt', server_side=True) + server.socket = ssl.wrap_socket( + server.socket, + keyfile="../web/localhost.key", + certfile='../web/localhost.crt', + server_side=True, + ) print('Saving signed firmware to firmware.json') - msg = get_firmware_object("signing_key.pem",HEX_FILE) - wfile = open('firmware.json','wb+') + msg = get_firmware_object("signing_key.pem", HEX_FILE) + wfile = open('firmware.json', 'wb+') wfile.write(json.dumps(msg).encode()) wfile.close() @@ -153,4 +161,3 @@ try: except KeyboardInterrupt: server.socket.close() - diff --git a/tools/nfcmon.py b/tools/nfcmon.py index 6b5d615..f0fc211 100644 --- a/tools/nfcmon.py +++ b/tools/nfcmon.py @@ -1,33 +1,35 @@ # # Copyright (C) 2018 SoloKeys, Inc. -# +# # This file is part of Solo. -# +# # Solo is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # Solo is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with Solo. If not, see -# +# # This code is available under licenses for commercial use. # Please contact SoloKeys for more information. # -import datetime,sys +import datetime, sys 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) @@ -43,7 +45,7 @@ else: chameleon.execCmd('LOGMODE=LIVE') while 1: - b = chameleon.read(1,20) + b = chameleon.read(1, 20) h = hexlify(b) h = h.decode() sys.stdout.write(h) diff --git a/tools/solotool.py b/tools/solotool.py index 3823c56..12bad67 100644 --- a/tools/solotool.py +++ b/tools/solotool.py @@ -24,10 +24,10 @@ # Programs solo using the Solo bootloader # Requires python-fido2, intelhex -import sys,os,time,struct,argparse -import array,struct,socket,json,base64,binascii +import sys, os, time, struct, argparse +import array, struct, socket, json, base64, binascii import tempfile -from binascii import hexlify,unhexlify +from binascii import hexlify, unhexlify from hashlib import sha256 from fido2.hid import CtapHidDevice, CTAPHID @@ -45,20 +45,23 @@ import serial def to_websafe(data): - data = data.replace('+','-') - data = data.replace('/','_') - data = data.replace('=','') + data = data.replace('+', '-') + data = data.replace('/', '_') + data = data.replace('=', '') return data + def from_websafe(data): - data = data.replace('-','+') - data = data.replace('_','/') - return data + '=='[:(3*len(data)) % 4] + 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 = open(hex_file, 'r').read() fw = base64.b64encode(fw.encode()) fw = to_websafe(fw.decode()) ih = IntelHex() @@ -66,18 +69,18 @@ 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() - arr = ih.tobinarray(start = START, size = END-START) + arr = ih.tobinarray(start=START, size=END - START) - im_size = END-START + im_size = END - START print('im_size: ', im_size) print('firmware_size: ', len(arr)) - byts = (arr).tobytes() if hasattr(arr,'tobytes') else (arr).tostring() + byts = (arr).tobytes() if hasattr(arr, 'tobytes') else (arr).tostring() h = sha256() h.update(byts) sig = binascii.unhexlify(h.hexdigest()) @@ -89,10 +92,11 @@ def get_firmware_object(sk_name, hex_file): sig = base64.b64encode(sig) sig = to_websafe(sig.decode()) - #msg = {'data': read()} - msg = {'firmware': fw, 'signature':sig} + # msg = {'data': read()} + msg = {'firmware': fw, 'signature': sig} return msg + class SoloBootloader: write = 0x40 done = 0x41 @@ -110,8 +114,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 @@ -123,7 +127,7 @@ class SoloClient(): def use_hid(self,): self.exchange = self.exchange_hid - def set_reboot(self,val): + def set_reboot(self, val): """ option to reboot after programming """ self.do_reboot = val @@ -145,8 +149,8 @@ class SoloClient(): self.send_data_hid(CTAPHID.INIT, '\x11\x11\x11\x11\x11\x11\x11\x11') @staticmethod - def format_request(cmd,addr = 0,data = b'A'*16): - arr = b'\x00'*9 + def format_request(cmd, addr=0, data=b'A' * 16): + arr = b'\x00' * 9 addr = struct.pack('H', len(data)) @@ -162,10 +166,10 @@ class SoloClient(): if type(data) != type(b''): data = struct.pack('%dB' % len(data), *[ord(x) for x in data]) with Timeout(1.0) as event: - return self.dev.call(cmd, data,event) + return self.dev.call(cmd, data, event) - def exchange_hid(self,cmd,addr=0,data=b'A'*16): - req = SoloClient.format_request(cmd,addr,data) + def exchange_hid(self, cmd, addr=0, data=b'A' * 16): + req = SoloClient.format_request(cmd, addr, data) data = self.send_data_hid(SoloBootloader.HIDCommandBoot, req) @@ -174,24 +178,24 @@ class SoloClient(): str = '' if ret == CtapError.ERR.NOT_ALLOWED: str = 'Out of bounds write' - raise RuntimeError('Device returned non-success code %02x: %s' % (ret,str)) + raise RuntimeError('Device returned non-success code %02x: %s' % (ret, str)) return data[1:] - def exchange_u2f(self,cmd,addr=0,data=b'A'*16): - appid = b'A'*32 - chal = b'B'*32 + def exchange_u2f(self, cmd, addr=0, data=b'A' * 16): + appid = b'A' * 32 + chal = b'B' * 32 - req = SoloClient.format_request(cmd,addr,data) + req = SoloClient.format_request(cmd, addr, data) - res = self.ctap1.authenticate(chal,appid, req) + res = self.ctap1.authenticate(chal, appid, req) ret = res.signature[0] if ret != CtapError.ERR.SUCCESS: str = '' if ret == CtapError.ERR.NOT_ALLOWED: str = 'Out of bounds write' - raise RuntimeError('Device returned non-success code %02x: %s' % (ret,str)) + raise RuntimeError('Device returned non-success code %02x: %s' % (ret, str)) return res.signature[1:] @@ -199,23 +203,23 @@ class SoloClient(): data = self.exchange(SoloBootloader.version) return data[0] - def write_flash(self,addr,data): - self.exchange(SoloBootloader.write,addr,data) + def write_flash(self, addr, data): + self.exchange(SoloBootloader.write, addr, data) - def get_rng(self,num=0): - ret = self.send_data_hid(SoloBootloader.HIDCommandRNG,struct.pack('B', num)) + def get_rng(self, num=0): + ret = self.send_data_hid(SoloBootloader.HIDCommandRNG, struct.pack('B', num)) return ret - def verify_flash(self,sig): + def verify_flash(self, sig): """ Tells device to check signature against application. If it passes, the application will boot. Exception raises if signature fails. """ - self.exchange(SoloBootloader.done,0,sig) + self.exchange(SoloBootloader.done, 0, sig) def wink(self,): - self.send_data_hid(CTAPHID.WINK,b'') + self.send_data_hid(CTAPHID.WINK, b'') def enter_solo_bootloader(self,): """ @@ -258,7 +262,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 @@ -266,11 +272,10 @@ class SoloClient(): self.exchange(SoloBootloader.do_reboot) return True - - def program_file(self,name): + def program_file(self, name): if name.lower().endswith('.json'): - data = json.loads(open(name,'r').read()) + data = json.loads(open(name, 'r').read()) fw = base64.b64decode(from_websafe(data['firmware']).encode()) sig = base64.b64decode(from_websafe(data['signature']).encode()) ih = IntelHex() @@ -294,44 +299,45 @@ class SoloClient(): seg = ih.segments()[0] size = seg[1] - seg[0] total = 0 - t1 = time.time()*1000 + t1 = time.time() * 1000 print('erasing...') for i in range(seg[0], seg[1], chunk): s = i - e = min(i+chunk,seg[1]) - data = ih.tobinarray(start=i,size = e-s) - self.write_flash(i,data) + e = min(i + chunk, seg[1]) + data = ih.tobinarray(start=i, size=e - s) + self.write_flash(i, data) total += chunk - progress = total/float(size)*100 + progress = total / float(size) * 100 sys.stdout.write('downloading %.2f%%...\r' % progress) sys.stdout.write('downloaded 100% \r\n') - t2 = time.time()*1000 - print('time: %.2f s' % ((t2-t1)/1000.0)) + t2 = time.time() * 1000 + print('time: %.2f s' % ((t2 - t1) / 1000.0)) print('Verifying...') if self.do_reboot: if sig is not None: self.verify_flash(sig) else: - self.verify_flash(b'A'*64) + self.verify_flash(b'A' * 64) + class DFU: class type: SEND = 0x21 - RECEIVE = 0xa1 + RECEIVE = 0xA1 class bmReq: - DETACH = 0x00 - DNLOAD = 0x01 - UPLOAD = 0x02 + DETACH = 0x00 + DNLOAD = 0x01 + UPLOAD = 0x02 GETSTATUS = 0x03 CLRSTATUS = 0x04 - GETSTATE = 0x05 - ABORT = 0x06 + GETSTATE = 0x05 + ABORT = 0x06 class state: APP_IDLE = 0x00 - APP_DETACH = 0x01 + APP_DETACH = 0x01 IDLE = 0x02 DOWNLOAD_SYNC = 0x03 DOWNLOAD_BUSY = 0x04 @@ -340,60 +346,64 @@ class DFU: MANIFEST = 0x07 MANIFEST_WAIT_RESET = 0x08 UPLOAD_IDLE = 0x09 - ERROR = 0x0a + ERROR = 0x0A class status: - def __init__(self,s): + def __init__(self, s): self.status = s[0] self.timeout = s[1] + (s[2] << 8) + (s[3] << 16) 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): + def addr2block(addr, size): addr -= 0x08000000 addr //= size addr += 2 return addr @staticmethod - def block2addr(addr,size): + def block2addr(addr, size): addr -= 2 addr *= size addr += 0x08000000 return addr - def find(self, altsetting = 0, ser=None): + def find(self, altsetting=0, ser=None): self.dev = None if ser: - devs = usb.core.find(idVendor=0x0483, idProduct=0xDF11,find_all=1) + devs = usb.core.find(idVendor=0x0483, idProduct=0xDF11, find_all=1) for x in devs: - if ser == (usb.util.get_string(x,x.iSerialNumber)): - print('connecting to ',ser) + if ser == (usb.util.get_string(x, x.iSerialNumber)): + print('connecting to ', ser) 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.') @@ -418,7 +428,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,): @@ -426,14 +438,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): + 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 @@ -441,19 +457,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): @@ -468,10 +486,10 @@ 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) + addr = DFUDevice.addr2block(addr, size) if self.state() not in (DFU.state.IDLE, DFU.state.UPLOAD_IDLE): self.clear_status() @@ -479,12 +497,12 @@ class DFUDevice: if self.state() not in (DFU.state.IDLE, DFU.state.UPLOAD_IDLE): raise RuntimeError('DFU device not in correct state for reading memory.') - return self.upload(addr,size) + return self.upload(addr, size) - def block_on_state(self,state): + def block_on_state(self, state): s = self.get_status() while s.state == state: - time.sleep(s.timeout/1000.0) + time.sleep(s.timeout / 1000.0) s = self.get_status() def detach(self,): @@ -503,7 +521,7 @@ class DFUDevice: def attempt_to_find_device(p): found = False - for i in range(0,5): + for i in range(0, 5): try: p.find_device() found = True @@ -512,6 +530,7 @@ def attempt_to_find_device(p): time.sleep(0.2) return found + def attempt_to_boot_bootloader(p): try: @@ -520,20 +539,26 @@ 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) + 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("--wink", action="store_true", help = 'HID Wink command.') + 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() p = SoloClient() @@ -549,35 +574,40 @@ def solo_main(): p.wink() sys.exit(0) + def asked_for_help(): - for i,v in enumerate(sys.argv): + 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( - """ + """ Reads serial output from USB serial port on Solo hacker. Automatically reconnects. usage: %s [-h] * 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) @@ -588,34 +618,37 @@ 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 - if asked_for_help() or len(sys.argv) not in (2,3): + if asked_for_help() or len(sys.argv) not in (2, 3): print( - """ + """ Generates key pair that can be used for Solo's signed firmware updates. usage: %s [input-seed-file] [-h] * Generates NIST P256 keypair. * 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: seed = sys.argv[2] print('using input seed file ', seed) - rng = open(seed,'rb').read() + rng = open(seed, 'rb').read() secexp = randrange_from_seed__trytryagain(rng, NIST256p.order) - sk = SigningKey.from_secret_exponent(secexp,curve = NIST256p) + sk = SigningKey.from_secret_exponent(secexp, curve=NIST256p) else: - sk = SigningKey.generate(curve = NIST256p) + sk = SigningKey.generate(curve=NIST256p) sk_name = sys.argv[1] - print('Signing key for signing device firmware: '+sk_name) - open(sk_name,'wb+').write(sk.to_pem()) + print('Signing key for signing device firmware: ' + sk_name) + open(sk_name, 'wb+').write(sk.to_pem()) vk = sk.get_verifying_key() @@ -623,31 +656,35 @@ def genkey_main(): print() print([c for c in vk.to_string()]) print() - print(''.join(['%02x'%c for c in vk.to_string()])) + print(''.join(['%02x' % c for c in vk.to_string()])) print() - print('"\\x' + '\\x'.join(['%02x'%c for c in vk.to_string()]) + '"') + 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 [-h]' % sys.argv[0]) print() sys.exit(1) - msg = get_firmware_object(sys.argv[1],sys.argv[2]) + msg = get_firmware_object(sys.argv[1], sys.argv[2]) print('Saving signed firmware to', sys.argv[3]) - wfile = open(sys.argv[3],'wb+') + wfile = open(sys.argv[3], 'wb+') wfile.write(json.dumps(msg).encode()) wfile.close() + def use_dfu(args): fw = args.__dict__['[firmware]'] - for i in range(0,8): + for i in range(0, 8): dfu = DFUDevice() try: - dfu.find(ser = args.dfu_serial) + dfu.find(ser=args.dfu_serial) except RuntimeError: time.sleep(0.25) dfu = None @@ -665,76 +702,124 @@ def use_dfu(args): chunk = 2048 seg = ih.segments()[0] - size = sum([max(x[1] - x[0],chunk) for x in ih.segments()]) + size = sum([max(x[1] - x[0], chunk) for x in ih.segments()]) total = 0 - t1 = time.time()*1000 + t1 = time.time() * 1000 print('erasing...') try: dfu.mass_erase() except usb.core.USBError: - dfu.write_page(0x08000000 + 2048*10,'ZZFF'*(2048//4)) + dfu.write_page(0x08000000 + 2048 * 10, 'ZZFF' * (2048 // 4)) dfu.mass_erase() page = 0 - for start,end in ih.segments(): + for start, end in ih.segments(): for i in range(start, end, chunk): page += 1 s = i - data = ih.tobinarray(start=i,size = chunk) - dfu.write_page(i,data) + data = ih.tobinarray(start=i, size=chunk) + dfu.write_page(i, data) total += chunk - progress = total/float(size)*100 + 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') # print(dfu.read_mem(i,16)) - t2 = time.time()*1000 + t2 = time.time() * 1000 print() - print('time: %d ms' %(t2 - t1)) + print('time: %d ms' % (t2 - t1)) print('verifying...') progress = 0 - for start,end in ih.segments(): + for start, end in ih.segments(): for i in range(start, end, chunk): - data1 = (dfu.read_mem(i,2048)) - data2 = ih.tobinarray(start=i,size = chunk) + 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)) - if (end-start) == chunk: - assert(data1 == data2) + progress = total / float(size) * 100 + sys.stdout.write( + 'reading %.2f%% %08x - %08x ... \r' + % (progress, i, i + page) + ) + if (end - start) == chunk: + 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: @@ -781,7 +866,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() @@ -794,20 +878,22 @@ def programmer_main(): print('Bootloader not active. Attempting to boot into bootloader mode...') attempt_to_boot_bootloader(p) else: - raise(e) + raise (e) except ApduError: print('Bootloader not active. Attempting to boot into bootloader mode...') attempt_to_boot_bootloader(p) if args.reset_only: - p.exchange(SoloBootloader.done,0,b'A'*64) + p.exchange(SoloBootloader.done, 0, b'A' * 64) else: p.program_file(fw) def main_mergehex(): if len(sys.argv) < 3: - print('usage: %s [...] [-s ] ') + print( + 'usage: %s [...] [-s ] ' + ) sys.exit(1) def flash_addr(num): @@ -816,54 +902,54 @@ 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): + for i, x in enumerate(args): if x == '-s': - secret_attestation_key = args[i+1] - args = args[:i] + args[i+2:] + secret_attestation_key = args[i + 1] + 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): + for i in range(2, len(args) - 1): print('merging %s with ' % (args[1]), args[i]) - first.merge(IntelHex( args[i] ), overlap = 'replace') + first.merge(IntelHex(args[i]), overlap='replace') - first [ flash_addr(APPLICATION_END_PAGE-1) ] = 0x41 - first [ flash_addr(APPLICATION_END_PAGE-1)+1 ] = 0x41 + first[flash_addr(APPLICATION_END_PAGE - 1)] = 0x41 + first[flash_addr(APPLICATION_END_PAGE - 1) + 1] = 0x41 - first[AUTH_WORD_ADDR-4] = 0 - first[AUTH_WORD_ADDR-1] = 0 - first[AUTH_WORD_ADDR-2] = 0 - first[AUTH_WORD_ADDR-3] = 0 + first[AUTH_WORD_ADDR - 4] = 0 + first[AUTH_WORD_ADDR - 1] = 0 + first[AUTH_WORD_ADDR - 2] = 0 + first[AUTH_WORD_ADDR - 3] = 0 - first[AUTH_WORD_ADDR] = 0 - first[AUTH_WORD_ADDR+1] = 0 - 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] = 0 + first[AUTH_WORD_ADDR + 1] = 0 + 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 if secret_attestation_key is not None: key = unhexlify(secret_attestation_key) - - for i,x in enumerate(key): + for i, x in enumerate(key): first[ATTEST_ADDR + i] = x - first.tofile(args[len(args)-1], format='hex') + first.tofile(args[len(args) - 1], format='hex') + if __name__ == '__main__': @@ -872,7 +958,7 @@ if __name__ == '__main__': print('usage: %s [options] [-h]' % sys.argv[0]) print('commands: program, solo, monitor, sign, genkey, mergehex') print( -""" + """ Examples: {0} program {0} program --use-dfu @@ -885,10 +971,12 @@ Examples: {0} sign {0} genkey [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 From 89de3738c01f968866d29cc334755ea561c978b0 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 14:30:19 +0100 Subject: [PATCH 03/15] Ensure python2 for env2 make target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c6398db..2d7ad47 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ uECC.o: ./crypto/micro-ecc/uECC.c $(CC) -c -o $@ $^ -O2 -fdata-sections -ffunction-sections -DuECC_PLATFORM=$(ecc_platform) -I./crypto/micro-ecc/ env2: - virtualenv env2 + virtualenv --python=python2.7 env2 env2/bin/pip install -r tools/requirements.txt env3: From c58c8300ac7bce2977040dca6d0ecf0e3a4c8184 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 14:53:44 +0100 Subject: [PATCH 04/15] Try and run some python tests in Travis --- .travis.yml | 24 ++++++++++++++---------- Makefile | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8d8a18f..c27c171 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ -dist: trusty -language: c -compiler: gcc -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - gcc-7 +sudo: required +dist: bionic +language: generic +compiler: + - clang + - gcc +cache: pip + +install: + - pip install -f tools/requirements.txt + - pip install black + script: - - export CC=gcc-7 + - env3/bin/black --check --skip-string-normalization tools/ + - env3/bin/python tools/solotool.py -h - make test diff --git a/Makefile b/Makefile index 2d7ad47..d9f7061 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ env3: # selectively reformat our own code black: env3 - env3/bin/black -S tools/ + env3/bin/black --skip-string-normalization tools/ wink2: env2 env2/bin/python tools/solotool.py solo --wink From 4136853e769a68992254ef59c865d7fd7b11f53c Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 14:57:50 +0100 Subject: [PATCH 05/15] And so the Travis dance begins --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index c27c171..344eb1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,12 @@ compiler: - clang - gcc cache: pip +python: 3 + +addons: + apt: + packages: + - python3 install: - pip install -f tools/requirements.txt From e63462cd867a6404dd60a89927acf6941fdd4a4f Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 15:00:09 +0100 Subject: [PATCH 06/15] ... --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 344eb1d..24699d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,7 @@ addons: - python3 install: - - pip install -f tools/requirements.txt - - pip install black + - make env3 script: - env3/bin/black --check --skip-string-normalization tools/ From b01b37d86b44254c87e2beee62cfa0b44f8477af Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 15:01:59 +0100 Subject: [PATCH 07/15] ... --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 24699d8..bcfb4fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ addons: apt: packages: - python3 + - python3-virtualenv install: - make env3 From a1d32050118a92e7af67dcb3993387d1383826a9 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 15:09:30 +0100 Subject: [PATCH 08/15] ... --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bcfb4fe..647b95f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,10 @@ addons: apt: packages: - python3 - - python3-virtualenv + - python3-venv install: + - env - make env3 script: From f2f71e2cbff05616fd8850066bc7c48fb0309bd7 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 15:15:23 +0100 Subject: [PATCH 09/15] ... --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 647b95f..4f8c7fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ addons: apt: packages: - python3 - - python3-venv + - python3.6-venv install: - env From c7f1aecd61366582acf1287c3ea1dccca2d939d5 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 15:16:57 +0100 Subject: [PATCH 10/15] ble --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f8c7fa..b1ead38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,9 @@ addons: install: - env - - make env3 + # - make env3 script: - - env3/bin/black --check --skip-string-normalization tools/ - - env3/bin/python tools/solotool.py -h + # - env3/bin/black --check --skip-string-normalization tools/ + # - env3/bin/python tools/solotool.py -h - make test From cbc422b8176da8b87399e513eca0d4957a5c75dc Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Thu, 3 Jan 2019 15:19:54 +0100 Subject: [PATCH 11/15] ble --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b1ead38..263e8e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ sudo: required dist: bionic language: generic -compiler: - - clang - - gcc +# compiler: +# - clang +# - gcc +compiler: gcc cache: pip python: 3 From 2f9987a28a36cdd61cbcaf415be2d0f326ecad3f Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Fri, 4 Jan 2019 02:01:59 +0100 Subject: [PATCH 12/15] be systematic with coding and shebang --- tools/convert_log_to_c.py | 2 ++ tools/ctap_test.py | 2 ++ tools/http2udb.py | 3 ++- tools/nfcmon.py | 2 ++ tools/solotool.py | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) mode change 100644 => 100755 tools/convert_log_to_c.py mode change 100644 => 100755 tools/ctap_test.py mode change 100644 => 100755 tools/http2udb.py mode change 100644 => 100755 tools/nfcmon.py mode change 100644 => 100755 tools/solotool.py diff --git a/tools/convert_log_to_c.py b/tools/convert_log_to_c.py old mode 100644 new mode 100755 index 5b54af4..5d6d2fa --- a/tools/convert_log_to_c.py +++ b/tools/convert_log_to_c.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. # diff --git a/tools/ctap_test.py b/tools/ctap_test.py old mode 100644 new mode 100755 index d137901..fbd2180 --- a/tools/ctap_test.py +++ b/tools/ctap_test.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. # diff --git a/tools/http2udb.py b/tools/http2udb.py old mode 100644 new mode 100755 index ecc5377..f037449 --- a/tools/http2udb.py +++ b/tools/http2udb.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. # diff --git a/tools/nfcmon.py b/tools/nfcmon.py old mode 100644 new mode 100755 index f0fc211..39c06a2 --- a/tools/nfcmon.py +++ b/tools/nfcmon.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. # diff --git a/tools/solotool.py b/tools/solotool.py old mode 100644 new mode 100755 index 12bad67..b9f4606 --- a/tools/solotool.py +++ b/tools/solotool.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. From 59503c775efa434432996af918768916a3f379a9 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Fri, 4 Jan 2019 02:02:27 +0100 Subject: [PATCH 13/15] minor changes --- Makefile | 6 +++--- README.md | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d9f7061..d72d90f 100644 --- a/Makefile +++ b/Makefile @@ -72,12 +72,12 @@ uECC.o: ./crypto/micro-ecc/uECC.c env2: virtualenv --python=python2.7 env2 - env2/bin/pip install -r tools/requirements.txt + env2/bin/pip install --upgrade -r tools/requirements.txt env3: python3 -m venv env3 - env3/bin/pip install -r tools/requirements.txt - env3/bin/pip install black + env3/bin/pip install --upgrade -r tools/requirements.txt + env3/bin/pip install --upgrade black # selectively reformat our own code black: env3 diff --git a/README.md b/README.md index d87faa8..fc229de 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ 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/stm32l442/src/led.c#L15) and force: ``` uint32_t b = 0; From 8210e9ecd9b279eac66c70d74db8f992d7686ba7 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Fri, 4 Jan 2019 04:28:05 +0100 Subject: [PATCH 14/15] give up on travis --- .travis.yml | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 263e8e9..8d8a18f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,12 @@ -sudo: required -dist: bionic -language: generic -# compiler: -# - clang -# - gcc +dist: trusty +language: c compiler: gcc -cache: pip -python: 3 - addons: apt: + sources: + - ubuntu-toolchain-r-test packages: - - python3 - - python3.6-venv - -install: - - env - # - make env3 - + - gcc-7 script: - # - env3/bin/black --check --skip-string-normalization tools/ - # - env3/bin/python tools/solotool.py -h + - export CC=gcc-7 - make test From a8e1060eeee41b1b0dbfc4049da65f3052f02173 Mon Sep 17 00:00:00 2001 From: Nicolas Stalder Date: Fri, 4 Jan 2019 04:30:00 +0100 Subject: [PATCH 15/15] fix some more scripts --- tools/gencert/cbytes.py | 1 + tools/gencert/dump_pem.py | 1 + tools/gencert/print_x_y.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/tools/gencert/cbytes.py b/tools/gencert/cbytes.py index 9c1f507..4520634 100644 --- a/tools/gencert/cbytes.py +++ b/tools/gencert/cbytes.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. # diff --git a/tools/gencert/dump_pem.py b/tools/gencert/dump_pem.py index ec78ca6..c313f19 100644 --- a/tools/gencert/dump_pem.py +++ b/tools/gencert/dump_pem.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. # diff --git a/tools/gencert/print_x_y.py b/tools/gencert/print_x_y.py index b527264..5083c49 100644 --- a/tools/gencert/print_x_y.py +++ b/tools/gencert/print_x_y.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright (C) 2018 SoloKeys, Inc. #