Skip to content

Commit

Permalink
Start refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
nickray committed Feb 15, 2019
1 parent a4bac85 commit 0fd85ef
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 340 deletions.
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# to use this, install [direnv](https://direnv.net/)
source venv/bin/activate
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
# Please consider
# https://help.github.com/articles/ignoring-files/#create-a-global-gitignore
# before adding to this file

dist/
venv/

15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.0.1a1] - 2019-02-15
### Changed
- Start to split out commands, helpers and client

## [0.0.1a0] - 2019-02-15
### Added
- Initial import of `solotool.py` script from [solo](https://github.com/solokeys/solo)
32 changes: 23 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
.PHONY: update-venv black clean

update-venv: venv
venv/bin/pip install -U pip
venv/bin/pip install -U -r dev-requirements.txt

venv:
python3 -m venv venv
venv/bin/pip install -U pip
.PHONY: update-venv black build clean publish

black:
black solo/

clean:
rm -rf venv
rm -rf dist


# Package management

build: black
flit build

reinstall:
pip uninstall -y solo-python
flit install

publish: build
flit publish

venv:
python3 -m venv venv
venv/bin/pip install -U pip

update-venv: venv
venv/bin/pip install -U pip
venv/bin/pip install -U -r dev-requirements.txt
File renamed without changes.
2 changes: 1 addition & 1 deletion solo/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.1-alpha
0.0.1a1
255 changes: 255 additions & 0 deletions solo/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
from solo.commands import SoloBootloader, SoloExtension
import solo.helpers


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 = next(CtapHidDevice.list_devices(), None)
if not dev:
raise RuntimeError("No FIDO device found")
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")

@staticmethod
def format_request(cmd, addr=0, data=b"A" * 16):
arr = b"\x00" * 9
addr = struct.pack("<L", addr)
cmd = struct.pack("B", cmd)
length = struct.pack(">H", len(data))

return cmd + addr[:3] + SoloBootloader.TAG + length + data

def send_only_hid(self, cmd, data):
if type(data) != type(b""):
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 type(data) != type(b""):
data = struct.pack("%dB" % len(data), *[ord(x) for x in data])
with Timeout(1.0) as event:
return self.dev.call(cmd, data, event)

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:
str = ""
if ret == CtapError.ERR.NOT_ALLOWED:
str = "Out of bounds write"
raise RuntimeError("Device returned non-success code %02x: %s" % (ret, str))

return data[1:]

def exchange_u2f(self, cmd, addr=0, data=b"A" * 16):
appid = b"A" * 32
chal = b"B" * 32

req = SoloClient.format_request(cmd, addr, data)

res = self.ctap1.authenticate(chal, appid, req)

ret = res.signature[0]
if ret != CtapError.ERR.SUCCESS:
str = ""
if ret == CtapError.ERR.NOT_ALLOWED:
str = "Out of bounds write"
raise RuntimeError("Device returned non-success code %02x: %s" % (ret, str))

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]

def solo_version(self,):
data = self.exchange_fido2(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 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...")
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("downloading %.2f%%...\r" % progress)
sys.stdout.write("downloaded 100% \r\n")
t2 = time.time() * 1000
print("time: %.2f s" % ((t2 - t1) / 1000.0))

print("Verifying...")
if self.do_reboot:
if sig is not None:
self.verify_flash(sig)
else:
self.verify_flash(b"A" * 64)

56 changes: 56 additions & 0 deletions solo/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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]
12 changes: 12 additions & 0 deletions solo/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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]

Loading

0 comments on commit 0fd85ef

Please sign in to comment.