PK hN cy solo/VERSION0.0.7 PKRNVXsolo/__init__.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. # """Python library for SoloKeys.""" import pathlib from . import client from . import commands from . import dfu from . import helpers from . import operations __version__ = open(pathlib.Path(__file__).parent / "VERSION").read().strip() del pathlib __all__ = ["client", "commands", "dfu", "enums", "exceptions", "helpers", "operations"] PKNhN\Ғ**solo/client.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import base64 import json import struct import sys import tempfile import time from cryptography import x509 from cryptography.hazmat.backends import default_backend from fido2.attestation import Attestation from fido2.client import Fido2Client from fido2.ctap import CtapError from fido2.ctap1 import CTAP1 from fido2.ctap2 import CTAP2 from fido2.hid import CtapHidDevice, CTAPHID from fido2.utils import Timeout from intelhex import IntelHex from solo.commands import SoloBootloader, SoloExtension import solo.exceptions from solo import helpers def find(solo_serial=None, retries=5, raw_device=None): # TODO: change `p` (for programmer) throughout p = SoloClient() # This... is not the right way to do it yet p.use_u2f() for i in range(retries): try: p.find_device(dev=raw_device, solo_serial=solo_serial) return p except RuntimeError: time.sleep(0.2) # return None raise solo.exceptions.NoSoloFoundError("no Solo found") def find_all(): hid_devices = list(CtapHidDevice.list_devices()) solo_devices = [ d for d in hid_devices if all( ( d.descriptor["vendor_id"] == 1155, d.descriptor["product_id"] == 41674, # "Solo" in d.descriptor["product_string"], ) ) ] return [find(raw_device=device) for device in solo_devices] class SoloClient: def __init__(self,): self.origin = "https://example.org" self.host = "example.org" self.exchange = self.exchange_hid self.do_reboot = True def use_u2f(self,): self.exchange = self.exchange_u2f def use_hid(self,): self.exchange = self.exchange_hid def set_reboot(self, val): """ option to reboot after programming """ self.do_reboot = val def reboot(self,): """ option to reboot after programming """ try: self.exchange(SoloBootloader.reboot) except OSError: pass def find_device(self, dev=None, solo_serial=None): if dev is None: devices = list(CtapHidDevice.list_devices()) if solo_serial is not None: devices = [ d for d in devices if d.descriptor["serial_number"] == solo_serial ] if len(devices) > 1: raise solo.exceptions.NonUniqueDeviceError if len(devices) == 0: raise RuntimeError("No FIDO device found") dev = devices[0] self.dev = dev self.ctap1 = CTAP1(dev) self.ctap2 = CTAP2(dev) self.client = Fido2Client(dev, self.origin) if self.exchange == self.exchange_hid: self.send_data_hid(CTAPHID.INIT, "\x11\x11\x11\x11\x11\x11\x11\x11") return self.dev @staticmethod def format_request(cmd, addr=0, data=b"A" * 16): # not sure why this is here? # arr = b"\x00" * 9 addr = struct.pack("H", len(data)) return cmd + addr[:3] + SoloBootloader.TAG + length + data def send_only_hid(self, cmd, data): if not isinstance(data, bytes): data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) self.dev._dev.InternalSend(0x80 | cmd, bytearray(data)) def send_data_hid(self, cmd, data): if not isinstance(data, bytes): 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) 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) ret = data[0] if ret != CtapError.ERR.SUCCESS: raise CtapError(ret) return data[1:] 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) res = self.ctap1.authenticate(chal, appid, req) ret = res.signature[0] if ret != CtapError.ERR.SUCCESS: raise CtapError(ret) return res.signature[1:] def exchange_fido2(self, cmd, addr=0, data=b"A" * 16): chal = "B" * 32 req = SoloClient.format_request(cmd, addr, data) assertions, client_data = self.client.get_assertion( self.host, chal, [{"id": req, "type": "public-key"}] ) if len(assertions) < 1: raise RuntimeError("Device didn't respond to FIDO2 extended assertion") res = assertions[0] ret = res.signature[0] if ret != CtapError.ERR.SUCCESS: raise RuntimeError("Device returned non-success code %02x" % (ret,)) return res.signature[1:] def bootloader_version(self,): data = self.exchange(SoloBootloader.version) if len(data) > 2: return (data[0], data[1], data[2]) return (data[0], 0, 0) def solo_version(self,): data = self.exchange(SoloExtension.version) return (data[0], data[1], data[2]) 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)) return ret 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) def wink(self,): self.send_data_hid(CTAPHID.WINK, b"") def reset(self,): self.ctap2.reset() def make_credential(self,): rp = {"id": self.host, "name": "example site"} user = {"id": b"abcdef", "name": "example user"} challenge = "Y2hhbGxlbmdl" attest, data = self.client.make_credential(rp, user, challenge, exclude_list=[]) try: attest.verify(data.hash) except AttributeError: verifier = Attestation.for_type(attest.fmt) verifier().verify(attest.att_statement, attest.auth_data, data.hash) print("Register valid") x5c = attest.att_statement["x5c"][0] cert = x509.load_der_x509_certificate(x5c, default_backend()) return cert def enter_solo_bootloader(self,): """ If solo is configured as solo hacker or something similar, this command will tell the token to boot directly to the bootloader so it can be reprogrammed """ if self.exchange != self.exchange_hid: self.send_data_hid(CTAPHID.INIT, "\x11\x11\x11\x11\x11\x11\x11\x11") self.send_data_hid(SoloBootloader.HIDCommandEnterBoot, "") def enter_bootloader_or_die(self): try: self.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) def is_solo_bootloader(self,): try: self.bootloader_version() return True except CtapError as e: if e.code == CtapError.ERR.INVALID_COMMAND: pass else: raise (e) return False def enter_st_dfu(self,): """ If solo is configured as solo hacker or something similar, this command will tell the token to boot directly to the st DFU so it can be reprogrammed. Warning, you could brick your device. """ soloboot = self.is_solo_bootloader() if soloboot or self.exchange == self.exchange_u2f: req = SoloClient.format_request(SoloBootloader.st_dfu) self.send_only_hid(SoloBootloader.HIDCommandBoot, req) else: self.send_only_hid(SoloBootloader.HIDCommandEnterSTBoot, "") def disable_solo_bootloader(self,): """ Disables the Solo bootloader. Only do this if you want to void the possibility 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 if ret[0] != CtapError.ERR.SUCCESS: print("Failed to disable bootloader") return False time.sleep(0.1) self.exchange(SoloBootloader.do_reboot) return True def program_file(self, name): if name.lower().endswith(".json"): data = json.loads(open(name, "r").read()) fw = base64.b64decode(helpers.from_websafe(data["firmware"]).encode()) sig = base64.b64decode(helpers.from_websafe(data["signature"]).encode()) ih = IntelHex() tmp = tempfile.NamedTemporaryFile(delete=False) tmp.write(fw) tmp.seek(0) tmp.close() ih.fromfile(tmp.name, format="hex") else: if not name.lower().endswith(".hex"): print('Warning, assuming "%s" is an Intel Hex file.' % name) sig = None ih = IntelHex() ih.fromfile(name, format="hex") if self.exchange == self.exchange_hid: chunk = 2048 else: chunk = 240 seg = ih.segments()[0] size = seg[1] - seg[0] total = 0 t1 = time.time() * 1000 print("erasing firmware...") 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) total += chunk progress = total / float(size) * 100 sys.stdout.write("updating firmware %.2f%%...\r" % progress) sys.stdout.write("updated firmware 100% \r\n") t2 = time.time() * 1000 print("time: %.2f s" % ((t2 - t1) / 1000.0)) if sig is None: sig = b"A" * 64 if self.do_reboot: self.verify_flash(sig) return sig PK<hNЀ'solo/commands.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. class SoloExtension: version = 0x14 rng = 0x15 class SoloBootloader: write = 0x40 done = 0x41 check = 0x42 erase = 0x43 version = 0x44 reboot = 0x45 st_dfu = 0x46 disable = 0x47 HIDCommandBoot = 0x50 HIDCommandEnterBoot = 0x51 HIDCommandEnterSTBoot = 0x52 HIDCommandRNG = 0x60 TAG = b"\x8C\x27\x90\xf6" class DFU: class type: SEND = 0x21 RECEIVE = 0xA1 class bmReq: DETACH = 0x00 DNLOAD = 0x01 UPLOAD = 0x02 GETSTATUS = 0x03 CLRSTATUS = 0x04 GETSTATE = 0x05 ABORT = 0x06 class state: APP_IDLE = 0x00 APP_DETACH = 0x01 IDLE = 0x02 DOWNLOAD_SYNC = 0x03 DOWNLOAD_BUSY = 0x04 DOWNLOAD_IDLE = 0x05 MANIFEST_SYNC = 0x06 MANIFEST = 0x07 MANIFEST_WAIT_RESET = 0x08 UPLOAD_IDLE = 0x09 ERROR = 0x0A class status: 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] PK<hN4T<11 solo/dfu.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import time import usb.core import usb.util import usb._objfinalizer from solo.commands import DFU import solo.exceptions # 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 def find(dfu_serial=None, attempts=8, raw_device=None): """dfu_serial is the ST bootloader serial number. It is not directly the ST chip identifier, but related via https://github.com/libopencm3/libopencm3/blob/master/lib/stm32/desig.c#L68 """ for i in range(attempts): dfu = DFUDevice() try: dfu.find(ser=dfu_serial, dev=raw_device) return dfu except RuntimeError: time.sleep(0.25) # return None raise Exception("no DFU found") def find_all(): st_dfus = usb.core.find(idVendor=0x0483, idProduct=0xDF11, find_all=True) return [find(raw_device=st_dfu) for st_dfu in st_dfus] class DFUDevice: def __init__(self,): pass @staticmethod def addr2list(a): return [a & 0xFF, (a >> 8) & 0xFF, (a >> 16) & 0xFF, (a >> 24) & 0xFF] @staticmethod def addr2block(addr, size): addr -= 0x08000000 addr //= size addr += 2 return addr @staticmethod def block2addr(addr, size): addr -= 2 addr *= size addr += 0x08000000 return addr def find(self, altsetting=0, ser=None, dev=None): if dev is not None: self.dev = dev else: if ser: devs = usb.core.find(idVendor=0x0483, idProduct=0xDF11, find_all=True) eligible = [ d for d in devs if ser == usb.util.get_string(d, d.iSerialNumber) ] if len(eligible) > 1: raise solo.exceptions.NonUniqueDeviceError if len(eligible) == 0: raise RuntimeError("No ST DFU devices found.") self.dev = eligible[0] print("connecting to ", ser) else: eligible = list( usb.core.find(idVendor=0x0483, idProduct=0xDF11, find_all=True) ) if len(eligible) > 1: raise solo.exceptions.NonUniqueDeviceError if len(eligible) == 0: raise RuntimeError("No ST DFU devices found.") self.dev = eligible[0] if self.dev is None: raise RuntimeError("No ST DFU devices found.") self.dev.set_configuration() for cfg in self.dev: for intf in cfg: if intf.bAlternateSetting == altsetting: intf.set_altsetting() self.intf = intf self.intNum = intf.bInterfaceNumber return self.dev raise RuntimeError("No ST DFU alternate-%d found." % altsetting) def init(self,): if self.state() == DFU.state.ERROR: self.clear_status() def close(self,): pass 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 ) return DFU.status(s) def state(self,): return self.get_status().state def clear_status(self,): # bmReqType, bmReq, wValue, wIndex, data/size _ = self.dev.ctrl_transfer( DFU.type.SEND, DFU.bmReq.CLRSTATUS, 0, self.intNum, None ) def upload(self, block, size): """ address is ((block – 2) × size) + 0x08000000 """ # bmReqType, bmReq, wValue, wIndex, data/size return self.dev.ctrl_transfer( DFU.type.RECEIVE, DFU.bmReq.UPLOAD, block, self.intNum, size ) def set_addr(self, addr): # must get_status after to take effect return self.dnload(0x0, [0x21] + DFUDevice.addr2list(addr)) 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 ) def erase(self, a): 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.block_on_state(DFU.state.DOWNLOAD_BUSY) 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): self.clear_status() self.clear_status() if self.state() not in (DFU.state.IDLE, DFU.state.DOWNLOAD_IDLE): raise RuntimeError("DFU device not in correct state for writing memory.") addr = DFUDevice.addr2block(addr, len(data)) # print('flashing %d bytes to block %d/%08x...' % (len(data), addr,oldaddr)) self.dnload(addr, data) self.block_on_state(DFU.state.DOWNLOAD_BUSY) assert DFU.state.DOWNLOAD_IDLE == self.state() def read_mem(self, addr, size): addr = DFUDevice.addr2block(addr, size) if self.state() not in (DFU.state.IDLE, DFU.state.UPLOAD_IDLE): self.clear_status() self.clear_status() 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) def block_on_state(self, state): s = self.get_status() while s.state == state: time.sleep(s.timeout / 1000.0) s = self.get_status() def detach(self,): if self.state() not in (DFU.state.IDLE, DFU.state.DOWNLOAD_IDLE): self.clear_status() self.clear_status() if self.state() not in (DFU.state.IDLE, DFU.state.DOWNLOAD_IDLE): raise RuntimeError("DFU device not in correct state for detaching.") # self.set_addr(0x08000000) # self.block_on_state(DFU.state.DOWNLOAD_BUSY) # assert(DFU.state.DOWNLOAD_IDLE == self.state()) self.dnload(0x0, []) return self.get_status() # return self.dev.ctrl_transfer(DFU.type.SEND, DFU.bmReq.DETACH, 0, self.intNum, None) PKRNyHP solo/enums.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. from enum import Enum class SoloMode(Enum): firmware = 1 bootloader = 2 dfu = 3 class SoloVariant(Enum): secure = 1 hacker = 2 PKt$SNF]solo/exceptions.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. class NonUniqueDeviceError(Exception): """When specifying a potentially destructive command... we check that either there is exactly one applicable device, or demand passing the serial number (same for ST DFU bootloader and Solo bootloader+firmware. """ pass class NoSoloFoundError(Exception): """Can signify no Solo, or missing udev rule on Linux.""" pass PKrhN5N||solo/helpers.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. 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] PKJPNsolo/operations.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import binascii from intelhex import IntelHex from solo import helpers def genkey(output_pem_file, input_seed_file=None): from ecdsa import SigningKey, NIST256p from ecdsa.util import randrange_from_seed__trytryagain if input_seed_file is not None: seed = input_seed_file 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 = output_pem_file print(f"Signing key for signing device firmware: {sk_name}") with open(sk_name, "wb+") as fh: fh.write(sk.to_pem()) vk = sk.get_verifying_key() return vk def mergehex(input_hex_files, output_hex_file, attestation_key=None): """Merges hex files, and patches in the attestation key. If no attestation key is passed, uses default Solo Hacker one. Note that later hex files replace data of earlier ones, if they overlap. """ if attestation_key is None: # generic / hacker attestation key attestation_key = ( "1b2626ecc8f69b0f69e34fb236d76466ba12ac16c3ab5750ba064e8b90e02448" ) # TODO put definitions somewhere else def flash_addr(num): return 0x08000000 + num * 2048 PAGES = 128 APPLICATION_END_PAGE = PAGES - 19 AUTH_WORD_ADDR = flash_addr(APPLICATION_END_PAGE) - 8 ATTEST_ADDR = flash_addr(PAGES - 15) first = IntelHex(input_hex_files[0]) for input_hex_file in input_hex_files[1:]: print(f"merging {first} with {input_hex_file}") first.merge(IntelHex(input_hex_file), overlap="replace") 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] = 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 # patch in the attestation key key = binascii.unhexlify(attestation_key) for i, x in enumerate(key): first[ATTEST_ADDR + i] = x first.tofile(output_hex_file, format="hex") def sign_firmware(sk_name, hex_file): # Maybe this is not the optimal module... import base64 import binascii from ecdsa import SigningKey from hashlib import sha256 from intelhex import IntelHex sk = SigningKey.from_pem(open(sk_name).read()) fw = open(hex_file, "r").read() fw = base64.b64encode(fw.encode()) fw = helpers.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 = helpers.to_websafe(sig.decode()) # msg = {'data': read()} msg = {"firmware": fw, "signature": sig} return msg PKmPN24((solo/solotool.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. # Programs solo using the Solo bootloader import sys, os, time, struct, argparse import array, struct, socket, json, base64, binascii import tempfile from binascii import hexlify, unhexlify from hashlib import sha256 import click from cryptography import x509 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend from fido2.hid import CtapHidDevice, CTAPHID from fido2.client import Fido2Client, ClientError from fido2.ctap import CtapError from fido2.ctap1 import CTAP1, ApduError from fido2.ctap2 import CTAP2 from fido2.utils import Timeout from fido2.attestation import Attestation import usb.core import usb._objfinalizer from intelhex import IntelHex import serial 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]"] p = solo.client.SoloClient() try: p.find_device() 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 [options] [-h]") print("commands: program, solo, monitor, sign, genkey, mergehex, version") print( """ Examples: {0} program {0} program --use-dfu {0} program --reboot {0} program --enter-bootloader {0} program --st-dfu {0} solo --wink {0} solo --rng {0} monitor {0} sign {0} genkey [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() PKXNȖsolo/cli/__init__.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import click import json import usb.core import solo import solo.operations from solo.cli.key import key from solo.cli.monitor import monitor from solo.cli.program import program from . import _patches # noqa (since otherwise "unused") @click.group() def solo_cli(): pass solo_cli.add_command(key) solo_cli.add_command(monitor) solo_cli.add_command(program) @click.command() def version(): """Version of python-solo library and tool.""" print(solo.__version__) solo_cli.add_command(version) @click.command() @click.option("--input-seed-file") @click.argument("output_pem_file") def genkey(input_seed_file, output_pem_file): """Generates key par that can be used for Solo signed firmware updates. \b * 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. """ vk = solo.operations.genkey(output_pem_file, input_seed_file=input_seed_file) 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() solo_cli.add_command(genkey) @click.command() @click.argument("verifying-key") @click.argument("app-hex") @click.argument("output-json") def sign(verifying_key, app_hex, output_json): """Signs a firmware hex file, outputs a .json file that can be used for signed update.""" msg = solo.operations.sign_firmware(verifying_key, app_hex) print("Saving signed firmware to", output_json) with open(output_json, "wb+") as fh: fh.write(json.dumps(msg).encode()) solo_cli.add_command(sign) @click.command() @click.option("--attestation-key", help="attestation key in hex") @click.argument("input_hex_files", nargs=-1) @click.argument("output_hex_file") def mergehex(attestation_key, input_hex_files, output_hex_file): """Merges hex files, and patches in the attestation key. \b If no attestation key is passed, uses default Solo Hacker one. Note that later hex files replace data of earlier ones, if they overlap. """ solo.operations.mergehex( input_hex_files, output_hex_file, attestation_key=attestation_key ) solo_cli.add_command(mergehex) @click.command() @click.option( "-a", "--all", is_flag=True, default=False, help="Show ST DFU devices too." ) def ls(all): """List Solos (in firmware or bootloader mode) and potential Solos in dfu mode.""" solos = solo.client.find_all() print(":: Solos") for c in solos: descriptor = c.dev.descriptor if "serial_number" in descriptor: print(f"{descriptor['serial_number']}: {descriptor['product_string']}") else: print(f"{descriptor['path']}: {descriptor['product_string']}") if all: print(":: Potential Solos in DFU mode") try: st_dfus = solo.dfu.find_all() for d in st_dfus: dev_raw = d.dev dfu_serial = dev_raw.serial_number print(f"{dfu_serial}") except usb.core.NoBackendError: print("No libusb available.") print( "This error is only relevant if you plan to use the ST DFU interface." ) print("If you are on Windows, please install a driver:") print("https://github.com/libusb/libusb/wiki/Windows#driver-installation") solo_cli.add_command(ls) PK%SNkr22solo/cli/_patches.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. """Monkey patch FIDO2 backend to get serial number.""" import sys ## Windows if sys.platform.startswith("win32"): import fido2._pyu2f.windows oldDevAttrFunc = fido2._pyu2f.windows.FillDeviceAttributes from ctypes import wintypes import ctypes fido2._pyu2f.windows.hid.HidD_GetSerialNumberString.restype = wintypes.BOOLEAN fido2._pyu2f.windows.hid.HidD_GetSerialNumberString.argtypes = [ ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong, ] def newDevAttrFunc(device, descriptor): oldDevAttrFunc(device, descriptor) buf_ser = ctypes.create_string_buffer(1024) result = fido2._pyu2f.windows.hid.HidD_GetSerialNumberString( device, buf_ser, 1024 ) if result: descriptor.serial_number = ctypes.wstring_at(buf_ser) fido2._pyu2f.windows.FillDeviceAttributes = newDevAttrFunc ## macOS if sys.platform.startswith("darwin"): import fido2._pyu2f.macos from fido2._pyu2f import base from fido2._pyu2f.macos import ( iokit, IO_HID_DEVICE_REF, GetDeviceIntProperty, GetDevicePath, GetDeviceStringProperty, HID_DEVICE_PROPERTY_VENDOR_ID, HID_DEVICE_PROPERTY_PRODUCT_ID, HID_DEVICE_PROPERTY_PRODUCT, HID_DEVICE_PROPERTY_PRIMARY_USAGE, HID_DEVICE_PROPERTY_PRIMARY_USAGE_PAGE, HID_DEVICE_PROPERTY_REPORT_ID, cf, ) HID_DEVICE_PROPERTY_SERIAL_NUMBER = b"SerialNumber" def newEnumerate(): """See base class.""" # Init a HID manager hid_mgr = iokit.IOHIDManagerCreate(None, None) if not hid_mgr: raise OSError("Unable to obtain HID manager reference") iokit.IOHIDManagerSetDeviceMatching(hid_mgr, None) # Get devices from HID manager device_set_ref = iokit.IOHIDManagerCopyDevices(hid_mgr) if not device_set_ref: raise OSError("Failed to obtain devices from HID manager") num = iokit.CFSetGetCount(device_set_ref) devices = (IO_HID_DEVICE_REF * num)() iokit.CFSetGetValues(device_set_ref, devices) # Retrieve and build descriptor dictionaries for each device descriptors = [] for dev in devices: d = base.DeviceDescriptor() d.vendor_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_VENDOR_ID) d.product_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_PRODUCT_ID) d.product_string = GetDeviceStringProperty(dev, HID_DEVICE_PROPERTY_PRODUCT) d.serial_number = GetDeviceStringProperty( dev, HID_DEVICE_PROPERTY_SERIAL_NUMBER ) d.usage = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_PRIMARY_USAGE) d.usage_page = GetDeviceIntProperty( dev, HID_DEVICE_PROPERTY_PRIMARY_USAGE_PAGE ) d.report_id = GetDeviceIntProperty(dev, HID_DEVICE_PROPERTY_REPORT_ID) d.path = GetDevicePath(dev) descriptors.append(d.ToPublicDict()) # Clean up CF objects cf.CFRelease(device_set_ref) cf.CFRelease(hid_mgr) return descriptors fido2._pyu2f.macos.MacOsHidDevice.Enumerate = newEnumerate ## Linux if sys.platform.startswith("linux"): import fido2._pyu2f.linux oldnewParseUevent = fido2._pyu2f.linux.ParseUevent def newParseUevent(uevent, desc): oldnewParseUevent(uevent, desc) lines = uevent.split(b"\n") for line in lines: line = line.strip() if not line: continue k, v = line.split(b"=") if k == b"HID_UNIQ": desc.serial_number = v.decode("utf8") fido2._pyu2f.linux.ParseUevent = newParseUevent PK}\NH >||solo/cli/key.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import sys import click from cryptography.hazmat.primitives import hashes from fido2.client import ClientError as Fido2ClientError from fido2.ctap1 import ApduError import solo from solo.cli.update import update import solo.fido2 # https://pocoo-click.readthedocs.io/en/latest/commands/#nested-handling-and-contexts @click.group() def key(): """Interact with Solo keys, see subcommands.""" pass @click.group() def rng(): """Access TRNG on key, see subcommands.""" pass @click.command() @click.option("--count", default=8, help="How many bytes to generate (defaults to 8)") @click.option("-s", "--serial", help="Serial number of Solo to use") def hexbytes(count, serial): """Output COUNT number of random bytes, hex-encoded.""" if not 0 <= count <= 255: print(f"Number of bytes must be between 0 and 255, you passed {count}") sys.exit(1) print(solo.client.find(serial).get_rng(count).hex()) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") def raw(serial): """Output raw entropy endlessly.""" p = solo.client.find(serial) while True: r = p.get_rng(255) sys.stdout.buffer.write(r) @click.command() @click.option("-s", "--serial", help="Serial number of Solo use") def reset(serial): """Reset key - wipes all credentials!!!""" if click.confirm( "Warning: Your credentials will be lost!!! Do you wish to continue?" ): print("Press the button to confirm -- again, your credentials will be lost!!!") solo.client.find(serial).reset() click.echo("....aaaand they're gone") @click.command() @click.option("-s", "--serial", help="Serial number of Solo use") @click.option( "--udp", is_flag=True, default=False, help="Communicate over UDP with software key" ) def verify(serial, udp): """Verify key is valid Solo Secure or Solo Hacker.""" if udp: solo.fido2.force_udp_backend() # Any longer and this needs to go in a submodule print("Please press the button on your Solo key") try: cert = solo.client.find(serial).make_credential() except Fido2ClientError: print("Error getting credential, is your key in bootloader mode?") print("Try: `solo program aux leave-bootloader`") sys.exit(1) solo_fingerprint = b"r\xd5\x831&\xac\xfc\xe9\xa8\xe8&`\x18\xe6AI4\xc8\xbeJ\xb8h_\x91\xb0\x99!\x13\xbb\xd42\x95" hacker_fingerprint = b"\xd0ml\xcb\xda}\xe5j\x16'\xc2\xa7\x89\x9c5\xa2\xa3\x16\xc8Q\xb3j\xd8\xed~\xd7\x84y\xbbx~\xf7" udp_fingerprint = b"\x05\x92\xe1\xb2\xba\x8ea\rb\x9a\x9b\xc0\x15\x19~J\xda\xdc16\xe0\xa0\xa1v\xd9\xb5}\x17\xa6\xb8\x0b8" if cert.fingerprint(hashes.SHA256()) == solo_fingerprint: print("Valid Solo Secure firmware from SoloKeys") elif cert.fingerprint(hashes.SHA256()) == hacker_fingerprint: print("Valid Solo Hacker firmware") elif cert.fingerprint(hashes.SHA256()) == udp_fingerprint: print("Local software key") else: print("Unknown fingerprint! ", cert.fingerprint(hashes.SHA256())) @click.command() @click.option("-s", "--serial", help="Serial number of Solo use") @click.option( "--udp", is_flag=True, default=False, help="Communicate over UDP with software key" ) def version(serial, udp): """Version of firmware on key.""" if udp: solo.fido2.force_udp_backend() try: major, minor, patch = solo.client.find(serial).solo_version() print(f"{major}.{minor}.{patch}") except solo.exceptions.NoSoloFoundError: print("No Solo found.") print("If you are on Linux, are your udev rules up to date?") except (solo.exceptions.NoSoloFoundError, ApduError): # Older print("Firmware is out of date (key does not know the SOLO_VERSION command).") @click.command() @click.option("-s", "--serial", help="Serial number of Solo use") @click.option( "--udp", is_flag=True, default=False, help="Communicate over UDP with software key" ) def wink(serial, udp): """Send wink command to key (blinks LED a few times).""" if udp: solo.fido2.force_udp_backend() solo.client.find(serial).wink() key.add_command(rng) rng.add_command(hexbytes) rng.add_command(raw) key.add_command(reset) key.add_command(update) key.add_command(version) key.add_command(verify) key.add_command(wink) PKPN]solo/cli/monitor.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import sys import time import click import serial @click.command() @click.argument("serial_port") def monitor(serial_port): """Reads Solo Hacker serial output from USB serial port SERIAL_PORT. SERIAL-PORT is something like /dev/ttyACM0 or COM10. Automatically reconnects. Baud rate is 115200. """ ser = serial.Serial(serial_port, 115200, timeout=0.05) def reconnect(): while True: time.sleep(0.02) try: ser = serial.Serial(serial_port, 115200, timeout=0.05) return ser except serial.SerialException: pass while True: try: data = ser.read(1) except serial.SerialException: print("reconnecting...") ser = reconnect() print("done") sys.stdout.buffer.write(data) sys.stdout.flush() PKahNӗsolo/cli/program.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import sys import time import click from fido2.ctap import CtapError import solo @click.group() def program(): """Program a key.""" pass # @click.command() # def ctap(): # """Program via CTAP (either CTAP1 or CTAP2) (assumes Solo bootloader).""" # pass # program.add_command(ctap) @click.command() @click.option("-s", "--serial", help="serial number of DFU to use") @click.option( "-a", "--connect-attempts", default=8, help="number of times to attempt connecting" ) # @click.option("--attach", default=False, help="Attempt switching to DFU before starting") @click.option( "-d", "--detach", default=False, is_flag=True, help="Reboot after successful programming", ) @click.option("-n", "--dry-run", is_flag=True, help="Just attach and detach") @click.argument("firmware") # , help="firmware (bundle) to program") def dfu(serial, connect_attempts, detach, dry_run, firmware): """Program via STMicroelectronics DFU interface. Enter dfu mode using `solo program aux enter-dfu` first. """ import time from intelhex import IntelHex import usb.core dfu = solo.dfu.find(serial, attempts=connect_attempts) if dfu is None: print("No STU DFU device found.") if serial is not None: print("Serial number used: ", serial) sys.exit(1) dfu.init() if not dry_run: # The actual programming # TODO: move to `operations.py` or elsewhere ih = IntelHex() ih.fromfile(firmware, format="hex") chunk = 2048 # Why is this unused # 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: # garbage write, sometimes needed before mass_erase 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 data = ih.tobinarray(start=i, size=chunk) dfu.write_page(i, data) total += chunk # here and below, progress would overshoot 100% otherwise progress = min(100, 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 = min(100, 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 detach: dfu.detach() program.add_command(dfu) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to wink") @click.argument("firmware") # , help="firmware (bundle) to program") def bootloader(serial, firmware): """Program via Solo bootloader interface. \b FIRMWARE argument should be either a .hex or .json file. If the bootloader is verifying, the .json is needed containing a signature for the verifying key in the bootloader. If the bootloader is nonverifying, either .hex or .json can be used. DANGER: if you try to flash a firmware with signature that doesn't match the bootloader's verifying key, you will be stuck in bootloader mode until you find a signed firmware that does match. Enter bootloader mode using `solo program aux enter-bootloader` first. """ p = solo.client.find(serial) try: p.use_hid() p.program_file(firmware) except CtapError as e: if e.code == CtapError.ERR.INVALID_COMMAND: print("Not in bootloader mode. Attempting to switch...") else: raise e p.enter_bootloader_or_die() print("Solo rebooted. Reconnecting...") time.sleep(0.5) p = solo.client.find(serial) if p is None: print("Cannot find Solo device.") sys.exit(1) p.use_hid() p.program_file(firmware) program.add_command(bootloader) @click.group() def aux(): """Auxiliary commands related to firmware/bootloader/dfu mode.""" pass program.add_command(aux) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to wink") def enter_bootloader(serial): """Switch from Solo firmware to Solo bootloader. Note that after powercycle, you will be in the firmware again, assuming it is valid. """ p = solo.client.find(serial) p.enter_bootloader_or_die() print("Solo rebooted. Reconnecting...") time.sleep(0.5) if solo.client.find(serial) is None: raise RuntimeError("Failed to reconnect!") aux.add_command(enter_bootloader) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to wink") def leave_bootloader(serial): """Switch from Solo bootloader to Solo firmware.""" p = solo.client.find(serial) # this is a bit too low-level... # p.exchange(solo.commands.SoloBootloader.done, 0, b"A" * 64) p.reboot() aux.add_command(leave_bootloader) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to wink") def enter_dfu(serial): """Switch from Solo bootloader to ST DFU bootloader. This changes the boot options of the key, which only reliably take effect after a powercycle. """ p = solo.client.find(serial) p.enter_st_dfu() # this doesn't really work yet ;) # p.reboot() print("Please powercycle the device (pull out, plug in again)") aux.add_command(enter_dfu) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to wink") def leave_dfu(serial): """Leave ST DFU bootloader. Switches to Solo bootloader or firmware, latter if firmware is valid. This changes the boot options of the key, which only reliably take effect after a powercycle. """ dfu = solo.dfu.find(serial) dfu.init() dfu.detach() print("Please powercycle the device (pull out, plug in again)") aux.add_command(leave_dfu) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to wink") def reboot(serial): """Reboot. \b This should reboot from anything (firmware, bootloader, DFU). Separately, need to be able to set boot options. """ # this implementation actually only works for bootloader # firmware doesn't have a reboot command solo.client.find(serial).reboot() aux.add_command(reboot) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to wink") def bootloader_version(serial): """Version of bootloader.""" p = solo.client.find(serial) print(".".join(map(str, p.bootloader_version()))) aux.add_command(bootloader_version) PKfhN)O O solo/cli/update.py# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import base64 import hashlib import json import sys import tempfile import click from fido2.ctap1 import ApduError import requests import solo from solo import helpers @click.command() @click.option("-s", "--serial", help="Serial number of Solo key to target") @click.option( "-y", "--yes", is_flag=True, help="Don't ask for confirmation before flashing" ) @click.option( "--hacker", is_flag=True, default=False, help="Use this flag to flash hacker build" ) @click.option( "--secure", is_flag=True, default=False, help="Use this flag to flash secure build" ) @click.option( "-lfs", "--local-firmware-server", is_flag=True, default=False, hidden=True, help="Development option: pull firmware from http://localhost:8000", ) @click.option( "--alpha", is_flag=True, default=False, hidden=True, help="Development option: use release refered to by ALPHA_VERSION", ) def update(serial, yes, hacker, secure, local_firmware_server, alpha): """Update Solo key to latest firmware version.""" # Check exactly one of --hacker/--secure is selected exactly_one_variant = len({hacker, secure}) == 2 if not exactly_one_variant: print("Please pass exactly one of `--hacker` or `--secure` as flag!") print("This flag should correspond to the key you are updating.") sys.exit(1) # Determine target key try: solo_client = solo.client.find(serial) except solo.exceptions.NoSoloFoundError: print() print("No Solo key found!") print() print("If you are on Linux, are your udev rules up to date?") print("Try adding a rule line such as the following:") print('ATTRS{idVendor}=="0483", ATTRS{idProduct}=="a2ca", TAG+="uaccess"') print("For more, see https://docs.solokeys.io/solo/udev/") print() sys.exit(1) except solo.exceptions.NonUniqueDeviceError: print() print("Multiple Solo keys are plugged in! Please:") # print(" * unplug all but one key, or") # print(" * specify target key via `--serial SERIAL_NUMBER`") print(" * unplug all but one key") print() sys.exit(1) except Exception: print() print("Unhandled error connecting to key.") print("Please report via https://github.com/solokeys/solo-python/issues/") print() sys.exit(1) # Ensure we are in bootloader mode try: solo_client.is_solo_bootloader() except (RuntimeError, ApduError): print("Please switch key to bootloader mode:") print("Unplug, hold button, plug in, wait for flashing yellow light.") sys.exit(1) # Have user confirm the targetted key is secure vs hacker # TODO: check out automatically (currently interface is too unstable to do this. variant = "Solo Hacker" if hacker else "Solo Secure" if not yes: print(f"We are about to update with the latest {variant} firmware.") click.confirm( f"Please confirm that the connected Solo key is a {variant}", abort=True ) # Get firmware version to use try: if alpha: version_file = "ALPHA_VERSION" else: version_file = "STABLE_VERSION" fetch_url = ( f"https://raw.githubusercontent.com/solokeys/solo/master/{version_file}" ) r = requests.get(fetch_url) if r.status_code != 200: print( f"Could not fetch version name from {version_file} in solokeys/solo repository!" ) sys.exit(1) version = r.text.split()[0].strip() # Windows BOM haha # if version.encode() == b'\xef\xbf\xbd\xef\xbf\xbd1\x00.\x001\x00.\x000\x00': # version = '1.1.0' try: assert version.count(".") == 2 major, minor, patch_and_more = version.split(".") if "-" in patch_and_more: patch, pre = patch_and_more.split("-") # noqa: F841 else: patch, pre = patch_and_more, None # noqa: F841 major, minor, patch = map(int, (major, minor, patch)) except Exception: print(f"Abnormal version format '{version}'") sys.exit(1) except Exception: print("Error fetching version name from solokeys/solo repository!") sys.exit(1) # Get firmware to use if local_firmware_server: base_url = "http://localhost:8000" else: base_url = f"https://github.com/solokeys/solo/releases/download/{version}" if hacker: firmware_file_github = f"firmware-hacker-{version}.hex" else: firmware_file_github = f"firmware-secure-{version}.json" firmware_url = f"{base_url}/{firmware_file_github}" extension = firmware_url.rsplit(".")[-1] try: r = requests.get(firmware_url) if r.status_code != 200: print( "Could not fetch official firmware build from solokeys/solo repository releases!" ) print(f"URL attempted: {firmware_url}") sys.exit(1) content = r.content if not hacker: try: # might as well use r.json() here too json_content = json.loads(content.decode()) except Exception: print(f"Invalid JSON content fetched from {firmware_url}!") sys.exit(1) with tempfile.NamedTemporaryFile(suffix="." + extension, delete=False) as fh: fh.write(r.content) firmware_file = fh.name print(f"Wrote temporary copy of {firmware_file_github} to {firmware_file}") except Exception: print("Problem fetching {firmware_url}!") sys.exit(1) # Check sha256sum m = hashlib.sha256() if hacker: m.update(content) else: firmware_content = base64.b64decode( helpers.from_websafe(json_content["firmware"]).encode() ) crlf_firmware_content = b"\r\n".join(firmware_content.split(b"\n")) m.update(crlf_firmware_content) our_digest = m.hexdigest() digest_url = firmware_url.rsplit(".", 1)[0] + ".sha2" official_digest = requests.get(digest_url).text.split()[0] if our_digest != official_digest: print( "sha256sum of downloaded firmware file does not coincide with published sha256sum!" ) print(f"sha256sum(downloaded): {our_digest}") print(f"sha256sum(published): {official_digest}") sys.exit(1) print(f"sha256sums coincide: {official_digest}") # Actually flash it... solo_client.use_hid() try: # We check the key accepted signature ourselves, # for more pertinent error messaging. solo_client.set_reboot(False) sig = solo_client.program_file(firmware_file) except Exception as e: print("problem flashing firmware!") print(e) raise sys.exit(1) try: print("bootloader is verifying signature...") solo_client.verify_flash(sig) print("...pass!") except Exception: print("...error!") print() print("Your key did not accept the firmware's signature! Possible reasons:") print(' * Tried to flash "hacker" firmware on secure key') print( ' * Tried to flash "hacker" firmware on custom hacker key with verifying bootloader' ) print() print( "Currently, your key does not work. Please run update again with correct parameters" ) sys.exit(1) # NB: There is a remaining error case: Flashing secure firmware on hacker key # will give rise to an incorrect attestation certificate. print() print( f"Congratulations, your {variant} was updated to the latest firmware version: {version}" ) PKDsTN solo/fido2/__init__.pyimport socket import fido2._pyu2f.base import fido2._pyu2f def force_udp_backend(): fido2._pyu2f.InternalPlatformSwitch = _UDP_InternalPlatformSwitch def _UDP_InternalPlatformSwitch(funcname, *args, **kwargs): if funcname == "__init__": return HidOverUDP(*args, **kwargs) return getattr(HidOverUDP, funcname)(*args, **kwargs) class HidOverUDP(fido2._pyu2f.base.HidDevice): @staticmethod def Enumerate(): a = [ { "vendor_id": 0x1234, "product_id": 0x5678, "product_string": "software test interface", "serial_number": "12345678", "usage": 0x01, "usage_page": 0xF1D0, "path": "localhost:8111", } ] return a def __init__(self, path): self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind(("127.0.0.1", 7112)) addr, port = path.split(":") port = int(port) self.token = (addr, port) self.sock.settimeout(1.0) def GetInReportDataLength(self): return 64 def GetOutReportDataLength(self): return 64 def Write(self, packet): self.sock.sendto(bytearray(packet), self.token) def Read(self): msg = [0] * 64 pkt, _ = self.sock.recvfrom(64) for i, v in enumerate(pkt): try: msg[i] = ord(v) except TypeError: msg[i] = v return msg PK!Hl%*,solo_python-0.0.7.dist-info/entry_points.txtN+I/N.,()mA^rNdpqPK-ONew((*solo_python-0.0.7.dist-info/LICENSE-APACHEApache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. PK-ONC#'solo_python-0.0.7.dist-info/LICENSE-MITPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HPO!solo_python-0.0.7.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!Hކ3CX$solo_python-0.0.7.dist-info/METADATAQN0+IJ+Q"Que9$ )@Dyћgp^h? sQol*ɑŽxHVR5.w4bpqt 4>&FAdyYUֳ$)Eu% G|2_IZ#1uD RCTK|WJ(`]4֙-<?s-Ya޸_c@O\vA#؂4[{sQo?,KgJǕSKo]k\濾Nu8_7 ͤ6x:{'klh3mt9rϻ`!r;wq`gS.p<PK!H<."solo_python-0.0.7.dist-info/RECORDӷX> M$!%{_Mf/ukj~),^Ӆ ޑ\pU07bif6'6(s@; ~ eu6zG~+ cAfSz(M; =Z >ʆ\`&8{ /SaE!'Jw V+in+46mTCd`$j^ 6΢eKid8$fSLzu H ھIzY~:,"^4#`vDb-IT#!Ax$M` 1Q|)C4./]7wdYHTQCumBDPN-(|gairˮ\k ]V̱6qTa\NuL>#̆*~&c\I9W=9 pVѰ! ^yQÙ04t. {!M|~N_QϺ7.\m_WӍ 3 k׹_B(7D ,$NֲG>-_yϬ]J}/mҙy ]_{ c vDn):cT>jcq)R$/M?PK hN cy solo/VERSIONPKRNVX0solo/__init__.pyPKNhN\Ғ**solo/client.pyPK<hNЀ'-solo/commands.pyPK<hN4T<11 3solo/dfu.pyPKRNyHP 1Osolo/enums.pyPKt$SNF]WQsolo/exceptions.pyPKrhN5N||vTsolo/helpers.pyPKJPNWsolo/operations.pyPKmPN24((fgsolo/solotool.pyPKXNȖsolo/cli/__init__.pyPK%SNkr22xsolo/cli/_patches.pyPK}\NH >||ܯsolo/cli/key.pyPKPN]solo/cli/monitor.pyPKahNӗsolo/cli/program.pyPKfhN)O O solo/cli/update.pyPKDsTN [solo/fido2/__init__.pyPK!Hl%*,| solo_python-0.0.7.dist-info/entry_points.txtPK-ONew((* solo_python-0.0.7.dist-info/LICENSE-APACHEPK-ONC#'B6solo_python-0.0.7.dist-info/LICENSE-MITPK!HPO!:solo_python-0.0.7.dist-info/WHEELPK!Hކ3CX$;solo_python-0.0.7.dist-info/METADATAPK!H<."<solo_python-0.0.7.dist-info/RECORDPK A