mirror of
https://github.com/solokeys/solo1-cli.git
synced 2026-03-11 17:14:15 -07:00
380 lines
10 KiB
Python
380 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright 2019 SoloKeys Developers
|
|
#
|
|
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
|
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
|
# http://opensource.org/licenses/MIT>, at your option. This file may not be
|
|
# copied, modified, or distributed except according to those terms.
|
|
|
|
# Programs solo using the Solo bootloader
|
|
import argparse
|
|
import array
|
|
import base64
|
|
import binascii
|
|
import json
|
|
import os
|
|
import socket
|
|
import struct
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from binascii import hexlify, unhexlify
|
|
from hashlib import sha256
|
|
|
|
import click
|
|
import serial
|
|
import usb._objfinalizer
|
|
import usb.core
|
|
from cryptography import x509
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes
|
|
from fido2.attestation import Attestation
|
|
from fido2.client import ClientError, Fido2Client
|
|
from fido2.ctap import CtapError
|
|
from fido2.ctap1 import CTAP1, ApduError
|
|
from fido2.ctap2 import CTAP2
|
|
from fido2.hid import CTAPHID, CtapHidDevice
|
|
from intelhex import IntelHex
|
|
|
|
import solo
|
|
from solo import helpers
|
|
|
|
|
|
def get_firmware_object(sk_name, hex_file):
|
|
# move to helpers
|
|
return helpers.sign_firmware(sk_name, hex_file)
|
|
|
|
|
|
def attempt_to_find_device(p):
|
|
found = False
|
|
for i in range(0, 5):
|
|
try:
|
|
p.find_device()
|
|
found = True
|
|
break
|
|
except RuntimeError:
|
|
time.sleep(0.2)
|
|
return found
|
|
|
|
|
|
def attempt_to_boot_bootloader(p):
|
|
|
|
try:
|
|
p.enter_solo_bootloader()
|
|
except OSError:
|
|
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."
|
|
)
|
|
sys.exit(1)
|
|
else:
|
|
raise (e)
|
|
print("Solo rebooted. Reconnecting...")
|
|
time.sleep(0.500)
|
|
if not attempt_to_find_device(p):
|
|
raise RuntimeError("Failed to reconnect!")
|
|
|
|
|
|
def solo_main():
|
|
# moved to new CLI
|
|
pass
|
|
|
|
|
|
def asked_for_help():
|
|
for i, v in enumerate(sys.argv):
|
|
if v == "-h" or v == "--help":
|
|
return True
|
|
return False
|
|
|
|
|
|
def monitor_main():
|
|
# moved to new CLI
|
|
pass
|
|
|
|
|
|
def genkey_main():
|
|
# moved to new CLI
|
|
pass
|
|
|
|
|
|
def sign_main():
|
|
# moved to new CLI
|
|
pass
|
|
|
|
|
|
def use_dfu(args):
|
|
fw = args.__dict__["[firmware]"]
|
|
|
|
for i in range(0, 8):
|
|
dfu = DFUDevice()
|
|
try:
|
|
dfu.find(ser=args.dfu_serial)
|
|
except RuntimeError:
|
|
time.sleep(0.25)
|
|
dfu = None
|
|
|
|
if dfu is None:
|
|
print("No STU DFU device found. ")
|
|
if args.dfu_serial:
|
|
print("Serial number used: ", args.dfu_serial)
|
|
sys.exit(1)
|
|
dfu.init()
|
|
|
|
if fw:
|
|
ih = IntelHex()
|
|
ih.fromfile(fw, format="hex")
|
|
|
|
chunk = 2048
|
|
seg = ih.segments()[0]
|
|
size = sum([max(x[1] - x[0], chunk) for x in ih.segments()])
|
|
total = 0
|
|
t1 = time.time() * 1000
|
|
|
|
print("erasing...")
|
|
try:
|
|
dfu.mass_erase()
|
|
except usb.core.USBError:
|
|
dfu.write_page(0x08000000 + 2048 * 10, "ZZFF" * (2048 // 4))
|
|
dfu.mass_erase()
|
|
|
|
page = 0
|
|
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)
|
|
total += chunk
|
|
progress = total / float(size) * 100
|
|
|
|
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
|
|
print()
|
|
print("time: %d ms" % (t2 - t1))
|
|
print("verifying...")
|
|
progress = 0
|
|
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)
|
|
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
|
|
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."
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
fw = args.__dict__["[firmware]"]
|
|
|
|
try:
|
|
p = solo.client.find()
|
|
if args.use_dfu:
|
|
print("entering dfu..")
|
|
try:
|
|
attempt_to_boot_bootloader(p)
|
|
p.enter_st_dfu()
|
|
except RuntimeError:
|
|
# already in DFU mode?
|
|
pass
|
|
except RuntimeError:
|
|
print("No Solo device detected.")
|
|
if fw or args.detach:
|
|
use_dfu(args)
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
|
|
if args.detach:
|
|
use_dfu(args)
|
|
sys.exit(0)
|
|
|
|
if args.use_u2f:
|
|
p.use_u2f()
|
|
|
|
if args.no_reset:
|
|
p.set_reboot(False)
|
|
|
|
if args.enter_bootloader:
|
|
print("Attempting to boot into bootloader mode...")
|
|
attempt_to_boot_bootloader(p)
|
|
sys.exit(0)
|
|
|
|
if args.reboot:
|
|
p.reboot()
|
|
sys.exit(0)
|
|
|
|
if args.st_dfu:
|
|
print("Sending command to boot into ST DFU...")
|
|
p.enter_st_dfu()
|
|
sys.exit(0)
|
|
|
|
if args.disable:
|
|
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()
|
|
sys.exit(1)
|
|
|
|
try:
|
|
p.bootloader_version()
|
|
except CtapError as e:
|
|
if e.code == CtapError.ERR.INVALID_COMMAND:
|
|
print("Bootloader not active. Attempting to boot into bootloader mode...")
|
|
attempt_to_boot_bootloader(p)
|
|
else:
|
|
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)
|
|
else:
|
|
p.program_file(fw)
|
|
|
|
|
|
def main_mergehex():
|
|
# moved to new CLI
|
|
pass
|
|
|
|
|
|
def main_version():
|
|
print(solo.__version__)
|
|
|
|
|
|
def main_main():
|
|
if sys.version_info[0] < 3:
|
|
print("Sorry, python3 is required.")
|
|
sys.exit(1)
|
|
|
|
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: solotool <command> [options] [-h]")
|
|
print("commands: program, solo, monitor, sign, genkey, mergehex, version")
|
|
print(
|
|
"""
|
|
Examples:
|
|
{0} program <filename.hex|filename.json>
|
|
{0} program <all.hex> --use-dfu
|
|
{0} program --reboot
|
|
{0} program --enter-bootloader
|
|
{0} program --st-dfu
|
|
{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]
|
|
{0} mergehex bootloader.hex solo.hex combined.hex
|
|
{0} version
|
|
""".format(
|
|
"solotool"
|
|
)
|
|
)
|
|
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()
|
|
elif c == "mergehex":
|
|
main_mergehex()
|
|
elif c == "version":
|
|
main_version()
|
|
else:
|
|
print("invalid command: %s" % c)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main_main()
|