combine into solotool.py for simplicity

This commit is contained in:
Conor Patrick 2018-12-31 14:27:15 -05:00
parent 4fe98ef560
commit d726465b67
5 changed files with 228 additions and 236 deletions

View File

@ -1,51 +0,0 @@
#
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
#
# 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 <https://www.gnu.org/licenses/>
#
# 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()

View File

@ -1,2 +1,4 @@
ecdsa ecdsa
intelhex intelhex
pyserial
python-fido2

View File

@ -1,62 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
#
# 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 <https://www.gnu.org/licenses/>
#
# 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 <serial-port>
* <serial-port> 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()

View File

@ -1,84 +0,0 @@
#
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
#
# 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 <https://www.gnu.org/licenses/>
#
# 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 <signing-key.pem> <app.hex> <output.json>' % 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()

View File

@ -1,21 +1,21 @@
# #
# Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/> # Copyright (C) 2018 SoloKeys, Inc. <https://solokeys.com/>
# #
# This file is part of Solo. # This file is part of Solo.
# #
# Solo is free software: you can redistribute it and/or modify # Solo is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Solo is distributed in the hope that it will be useful, # Solo is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Solo. If not, see <https://www.gnu.org/licenses/> # along with Solo. If not, see <https://www.gnu.org/licenses/>
# #
# This code is available under licenses for commercial use. # This code is available under licenses for commercial use.
# Please contact SoloKeys for more information. # Please contact SoloKeys for more information.
# #
@ -24,9 +24,10 @@
# Requires python-fido2, intelhex # Requires python-fido2, intelhex
import sys,os,time,struct,argparse import sys,os,time,struct,argparse
import array,struct,socket,json,base64 import array,struct,socket,json,base64,binascii
import tempfile import tempfile
from binascii import hexlify from binascii import hexlify
from hashlib import sha256
from fido2.hid import CtapHidDevice, CTAPHID from fido2.hid import CtapHidDevice, CTAPHID
from fido2.client import Fido2Client, ClientError from fido2.client import Fido2Client, ClientError
@ -35,8 +36,57 @@ from fido2.ctap1 import CTAP1, ApduError
from fido2.utils import Timeout from fido2.utils import Timeout
from intelhex import IntelHex 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: class SoloBootloader:
write = 0x40 write = 0x40
@ -55,12 +105,12 @@ class SoloBootloader:
TAG = b'\x8C\x27\x90\xf6' TAG = b'\x8C\x27\x90\xf6'
class Programmer(): class SoloClient():
def __init__(self,): def __init__(self,):
self.origin = 'https://example.org' self.origin = 'https://example.org'
self.exchange = self.exchange_hid self.exchange = self.exchange_hid
self.reboot = True self.do_reboot = True
def use_u2f(self,): def use_u2f(self,):
self.exchange = self.exchange_u2f self.exchange = self.exchange_u2f
@ -70,9 +120,9 @@ class Programmer():
def set_reboot(self,val): def set_reboot(self,val):
""" option to reboot after programming """ """ option to reboot after programming """
self.reboot = val self.do_reboot = val
def reboot(self,val): def reboot(self,):
""" option to reboot after programming """ """ option to reboot after programming """
try: try:
self.exchange(SoloBootloader.reboot) self.exchange(SoloBootloader.reboot)
@ -110,7 +160,7 @@ class Programmer():
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): 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) data = self.send_data_hid(SoloBootloader.HIDCommandBoot, req)
@ -124,7 +174,7 @@ class Programmer():
appid = b'A'*32 appid = b'A'*32
chal = b'B'*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) res = self.ctap1.authenticate(chal,appid, req)
@ -186,7 +236,7 @@ class Programmer():
soloboot = self.is_solo_bootloader() soloboot = self.is_solo_bootloader()
if soloboot or self.exchange == self.exchange_u2f: 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) self.send_only_hid(SoloBootloader.HIDCommandBoot, req)
else: else:
self.send_only_hid(SoloBootloader.HIDCommandEnterSTBoot, '') self.send_only_hid(SoloBootloader.HIDCommandEnterSTBoot, '')
@ -202,7 +252,7 @@ class Programmer():
print('Failed to disable bootloader') print('Failed to disable bootloader')
return False return False
time.sleep(0.1) time.sleep(0.1)
self.exchange(SoloBootloader.reboot) self.exchange(SoloBootloader.do_reboot)
return True return True
@ -248,7 +298,7 @@ class Programmer():
print('time: %.2f s' % ((t2-t1)/1000.0)) print('time: %.2f s' % ((t2-t1)/1000.0))
print('Verifying...') print('Verifying...')
if self.reboot: if self.do_reboot:
if sig is not None: if sig is not None:
self.verify_flash(sig) self.verify_flash(sig)
else: else:
@ -283,7 +333,118 @@ def attempt_to_boot_bootloader(p):
print('Failed to reconnect!') print('Failed to reconnect!')
sys.exit(1) 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 <serial-port> [-h]
* <serial-port> 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 <output-pem-file> [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 <signing-key.pem> <app.hex> <output.json> [-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 = 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("[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("--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("--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("--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() args = parser.parse_args()
print()
p = Programmer() p = SoloClient()
p.find_device() p.find_device()
if args.use_u2f: if args.use_u2f:
@ -317,16 +475,6 @@ if __name__ == '__main__':
p.reboot() p.reboot()
sys.exit(0) 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: if args.st_dfu:
print('Sending command to boot into ST DFU...') print('Sending command to boot into ST DFU...')
p.enter_st_dfu() p.enter_st_dfu()
@ -336,6 +484,12 @@ if __name__ == '__main__':
p.disable_solo_bootloader() p.disable_solo_bootloader()
sys.exit(0) 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: try:
p.version() p.version()
except CtapError as e: except CtapError as e:
@ -346,12 +500,45 @@ if __name__ == '__main__':
except ApduError: except ApduError:
attempt_to_boot_bootloader(p) attempt_to_boot_bootloader(p)
if not args.reset_only: if 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:
p.exchange(SoloBootloader.done,0,b'A'*64) 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 <command> [options] [-h]' % sys.argv[0])
print('commands: program, solo, monitor, sign, genkey')
print(
"""
Examples:
{0} program <filename.hex|filename.json>
{0} program --reboot
{0} program --enter-bootloader
{0} solo --wink
{0} solo --rng
{0} monitor <serial-port>
{0} sign <key.pem> <firmware.hex> <output.json>
{0} genkey <output-pem-file> [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)