From d726465b678ac310cbbd5faf30ede79ccca692d1 Mon Sep 17 00:00:00 2001 From: Conor Patrick Date: Mon, 31 Dec 2018 14:27:15 -0500 Subject: [PATCH] combine into solotool.py for simplicity --- tools/gen_keys.py | 51 -------- tools/requirements.txt | 2 + tools/serial_monitor.py | 62 ---------- tools/sign_firmware.py | 84 ------------- tools/solotool.py | 265 ++++++++++++++++++++++++++++++++++------ 5 files changed, 228 insertions(+), 236 deletions(-) delete mode 100644 tools/gen_keys.py delete mode 100644 tools/serial_monitor.py delete mode 100644 tools/sign_firmware.py diff --git a/tools/gen_keys.py b/tools/gen_keys.py deleted file mode 100644 index 62b1b29..0000000 --- a/tools/gen_keys.py +++ /dev/null @@ -1,51 +0,0 @@ -# -# 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 ecdsa import SigningKey, NIST256p -from ecdsa.util import randrange_from_seed__trytryagain -import sys - -if len(sys.argv) > 1: - print('using input seed file ', sys.argv[1]) - rng = open(sys.argv[1],'rb').read() - secexp = randrange_from_seed__trytryagain(rng, NIST256p.order) - sk = SigningKey.from_secret_exponent(secexp,curve = NIST256p) -else: - sk = SigningKey.generate(curve = NIST256p) - - - -sk_name = 'signing_key.pem' -print('Signing key for signing device firmware: '+sk_name) -open(sk_name,'wb+').write(sk.to_pem()) - -vk = sk.get_verifying_key() - -print('Public key in various formats:') -print() -print([c for c in vk.to_string()]) -print() -print(''.join(['%02x'%c for c in vk.to_string()])) -print() -print('"\\x' + '\\x'.join(['%02x'%c for c in vk.to_string()]) + '"') -print() - - diff --git a/tools/requirements.txt b/tools/requirements.txt index ae093c8..88b4e28 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,2 +1,4 @@ ecdsa intelhex +pyserial +python-fido2 diff --git a/tools/serial_monitor.py b/tools/serial_monitor.py deleted file mode 100644 index 473782b..0000000 --- a/tools/serial_monitor.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/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. -# - -# This is a basic, cross-platfrom serial emulator. -# It will automatically try to reconnect to a serial port that disconnects. -# Ideal for development with Solo. -# -# Requires pySerial -# -import sys,time -import serial - -if len(sys.argv) != 2: - print( -""" -usage: %s - * will look like COM10 or /dev/ttyACM0 or something. - * baud is 115200. -""" % sys.argv[0]) - sys.exit(1) - -port = sys.argv[1] - -ser = serial.Serial(port,115200,timeout=.05) - -def reconnect(): - while(1): - time.sleep(0.02) - try: - ser = serial.Serial(port,115200,timeout=.05) - return ser - except serial.SerialException: - pass -while 1: - try: - d = ser.read(1) - except serial.SerialException: - print('reconnecting...') - ser = reconnect() - print('done') - sys.stdout.buffer.write(d) - sys.stdout.flush() diff --git a/tools/sign_firmware.py b/tools/sign_firmware.py deleted file mode 100644 index b5935ee..0000000 --- a/tools/sign_firmware.py +++ /dev/null @@ -1,84 +0,0 @@ -# -# 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 sys -import json,base64,array,binascii -from hashlib import sha256 - -from ecdsa import SigningKey, NIST256p -from intelhex import IntelHex - -def to_websafe(data): - 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] - -def get_firmware_object(sk_name, hex_file): - sk = SigningKey.from_pem(open(sk_name).read()) - fw = open(hex_file,'r').read() - fw = base64.b64encode(fw.encode()) - fw = to_websafe(fw.decode()) - ih = IntelHex() - ih.fromfile(hex_file, format='hex') - # 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) - - ih = IntelHex(hex_file) - segs = ih.segments() - arr = ih.tobinarray(start = START, 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() - h = sha256() - h.update(byts) - sig = binascii.unhexlify(h.hexdigest()) - print('hash', binascii.hexlify(sig)) - sig = sk.sign_digest(sig) - - print('sig', binascii.hexlify(sig)) - - sig = base64.b64encode(sig) - sig = to_websafe(sig.decode()) - - #msg = {'data': read()} - msg = {'firmware': fw, 'signature':sig} - return msg - -if __name__ == '__main__': - if len(sys.argv) != 4: - print('usage: %s ' % sys.argv[0]) - 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.write(json.dumps(msg).encode()) - wfile.close() diff --git a/tools/solotool.py b/tools/solotool.py index 9165d8b..c1faa73 100644 --- a/tools/solotool.py +++ b/tools/solotool.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. # @@ -24,9 +24,10 @@ # Requires python-fido2, intelhex import sys,os,time,struct,argparse -import array,struct,socket,json,base64 +import array,struct,socket,json,base64,binascii import tempfile from binascii import hexlify +from hashlib import sha256 from fido2.hid import CtapHidDevice, CTAPHID from fido2.client import Fido2Client, ClientError @@ -35,8 +36,57 @@ from fido2.ctap1 import CTAP1, ApduError from fido2.utils import Timeout from intelhex import IntelHex +import serial -from sign_firmware import * + +def to_websafe(data): + 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] + +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()) + fw = to_websafe(fw.decode()) + ih = IntelHex() + ih.fromfile(hex_file, format='hex') + # 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) + + ih = IntelHex(hex_file) + segs = ih.segments() + arr = ih.tobinarray(start = START, 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() + h = sha256() + h.update(byts) + sig = binascii.unhexlify(h.hexdigest()) + print('hash', binascii.hexlify(sig)) + sig = sk.sign_digest(sig) + + print('sig', binascii.hexlify(sig)) + + sig = base64.b64encode(sig) + sig = to_websafe(sig.decode()) + + #msg = {'data': read()} + msg = {'firmware': fw, 'signature':sig} + return msg class SoloBootloader: write = 0x40 @@ -55,12 +105,12 @@ class SoloBootloader: TAG = b'\x8C\x27\x90\xf6' -class Programmer(): +class SoloClient(): def __init__(self,): self.origin = 'https://example.org' self.exchange = self.exchange_hid - self.reboot = True + self.do_reboot = True def use_u2f(self,): self.exchange = self.exchange_u2f @@ -70,9 +120,9 @@ class Programmer(): def set_reboot(self,val): """ option to reboot after programming """ - self.reboot = val + self.do_reboot = val - def reboot(self,val): + def reboot(self,): """ option to reboot after programming """ try: self.exchange(SoloBootloader.reboot) @@ -110,7 +160,7 @@ class Programmer(): return self.dev.call(cmd, data,event) def exchange_hid(self,cmd,addr=0,data=b'A'*16): - req = Programmer.format_request(cmd,addr,data) + req = SoloClient.format_request(cmd,addr,data) data = self.send_data_hid(SoloBootloader.HIDCommandBoot, req) @@ -124,7 +174,7 @@ class Programmer(): appid = b'A'*32 chal = b'B'*32 - req = Programmer.format_request(cmd,addr,data) + req = SoloClient.format_request(cmd,addr,data) res = self.ctap1.authenticate(chal,appid, req) @@ -186,7 +236,7 @@ class Programmer(): soloboot = self.is_solo_bootloader() if soloboot or self.exchange == self.exchange_u2f: - req = Programmer.format_request(SoloBootloader.st_dfu) + req = SoloClient.format_request(SoloBootloader.st_dfu) self.send_only_hid(SoloBootloader.HIDCommandBoot, req) else: self.send_only_hid(SoloBootloader.HIDCommandEnterSTBoot, '') @@ -202,7 +252,7 @@ class Programmer(): print('Failed to disable bootloader') return False time.sleep(0.1) - self.exchange(SoloBootloader.reboot) + self.exchange(SoloBootloader.do_reboot) return True @@ -248,7 +298,7 @@ class Programmer(): print('time: %.2f s' % ((t2-t1)/1000.0)) print('Verifying...') - if self.reboot: + if self.do_reboot: if sig is not None: self.verify_flash(sig) else: @@ -283,7 +333,118 @@ def attempt_to_boot_bootloader(p): print('Failed to reconnect!') sys.exit(1) -if __name__ == '__main__': +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.') + args = parser.parse_args() + + p = SoloClient() + p.find_device() + + if args.rng: + while True: + r = p.get_rng(255) + sys.stdout.buffer.write(r) + sys.exit(0) + + if args.wink: + 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( + """ + 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.exit(1) + + port = sys.argv[1] + + ser = serial.Serial(port,115200,timeout=.05) + + def reconnect(): + while(1): + time.sleep(0.02) + try: + ser = serial.Serial(port,115200,timeout=.05) + return ser + except serial.SerialException: + pass + while 1: + try: + d = ser.read(1) + except serial.SerialException: + print('reconnecting...') + ser = reconnect() + print('done') + 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): + 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.exit(1) + + if len(sys.argv) > 2: + seed = sys.argv[2] + print('using input seed file ', seed) + rng = open(seed,'rb').read() + secexp = randrange_from_seed__trytryagain(rng, NIST256p.order) + sk = SigningKey.from_secret_exponent(secexp,curve = NIST256p) + else: + 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()) + + vk = sk.get_verifying_key() + + print('Public key in various formats:') + print() + print([c for c in vk.to_string()]) + print() + print(''.join(['%02x'%c for c in vk.to_string()])) + print() + 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('usage: %s [-h]' % sys.argv[0]) + print() + sys.exit(1) + 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.write(json.dumps(msg).encode()) + wfile.close() + +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.') @@ -295,12 +456,9 @@ if __name__ == '__main__': 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("--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() - print() - p = Programmer() + p = SoloClient() p.find_device() if args.use_u2f: @@ -317,16 +475,6 @@ if __name__ == '__main__': p.reboot() sys.exit(0) - if args.rng: - while True: - r = p.get_rng(255) - sys.stdout.buffer.write(r) - sys.exit(0) - - if args.wink: - p.wink() - sys.exit(0) - if args.st_dfu: print('Sending command to boot into ST DFU...') p.enter_st_dfu() @@ -336,6 +484,12 @@ if __name__ == '__main__': p.disable_solo_bootloader() sys.exit(0) + fw = args.__dict__['[firmware]'] + if fw == '': + print('Need to supply firmware filename, or see help for more options.') + parser.print_help() + sys.exit(1) + try: p.version() except CtapError as e: @@ -346,12 +500,45 @@ if __name__ == '__main__': except ApduError: attempt_to_boot_bootloader(p) - if not args.reset_only: - fw = args.__dict__['[firmware]'] - if fw == '': - print('Need to supply firmware filename.') - args.print_help() - sys.exit(1) - p.program_file(fw) - else: + if args.reset_only: p.exchange(SoloBootloader.done,0,b'A'*64) + else: + p.program_file(fw) + +if __name__ == '__main__': + + if len(sys.argv) < 2 or (len(sys.argv) == 2 and asked_for_help()): + print('Diverse command line tool for working with Solo') + print('usage: %s [options] [-h]' % sys.argv[0]) + print('commands: program, solo, monitor, sign, genkey') + print( +""" +Examples: + {0} program + {0} program --reboot + {0} program --enter-bootloader + {0} solo --wink + {0} solo --rng + {0} monitor + {0} sign + {0} genkey [rng-seed-file] +""".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 + + if c == 'program': + programmer_main() + elif c == 'solo': + solo_main() + elif c == 'monitor': + monitor_main() + elif c == 'sign': + sign_main() + elif c == 'genkey': + genkey_main() + else: + print('invalid command: %s' % c)