Compare commits

...

49 Commits

Author SHA1 Message Date
Chris Sulmone c1a5abc6a9 Fixed exception: 'TypeError: argument must be string or read-only buffer, not bytearray' 2018-03-16 00:46:08 -05:00
BTChip github ab589175f3
Better interface filter, OSX portability 2018-03-12 19:21:16 +01:00
BTChip github ede307f9d0
More Python 3 tweaks 2018-03-12 11:55:08 +01:00
Nicolas Bacca 01ac906758
Merge pull request #14 from NicolasDorier/errorfriendly
More friendly error messages
2018-03-07 14:44:12 +01:00
BTChip github 3242f93051
More Python 3 tweaks 2018-03-07 13:26:40 +01:00
BTChip github 1e9458ca58
Add 1.4 Target ID 2018-03-06 10:26:05 +01:00
Nicolas Bacca 3a8a32a2ee
Merge pull request #28 from jeffesquivels/readme_fix
Correct pip parameters order for secp256k1
2018-03-06 10:23:42 +01:00
BTChip github e834117229
Nano S 1.4 support, cleanup Python 3 support 2018-03-06 10:21:37 +01:00
Jeffrey Esquivel S ad385223a3 Correct pip parameters order for secp256k1
Fixes the order of the parameters given to pip install for installing
secp256k1.
2018-02-11 10:43:09 -06:00
BTChip github 9914b3746a
Bump version 2017-10-20 15:11:13 +02:00
BTChip github 2abf1cd983
Fix invalid exit condition, courtesy of Andrew Poelstra/Rusy Russell 2017-10-20 15:10:58 +02:00
BTChip github ed1e3a4dac
Python 3 fixes 2017-10-20 15:09:24 +02:00
NicolasDorier b66f66b9bf More error friendly errors 2017-09-11 19:53:20 +09:00
Nicolas Bacca a9611d412a Merge pull request #10 from parkerhoyes/master
Fixed CSS override issue for docs
2017-08-01 17:49:11 +02:00
Parker Hoyes b23172e440 Fixed CSS override issue for docs 2017-08-01 11:39:18 -04:00
BTChip github bf1e3453cc
Bump version for the documentation 2017-08-01 16:59:06 +02:00
Nicolas Bacca 214cd9783c Merge pull request #9 from parkerhoyes/master
Added documentation
2017-08-01 16:51:47 +02:00
Parker Hoyes 1ad7bb24e7 Added documentation 2017-08-01 10:41:12 -04:00
BTChip github ba72aa7fb0
Add runApp.py for specific firmware, cleanup 2017-03-07 18:25:19 +01:00
BTChip github 09ad4fd1e2
Add scripts related to Nano S 1.3 2017-02-28 16:13:41 +01:00
BTChip 310442593d Additional Attestation / Endorsement related APIs, merge HSM logic 2017-01-30 19:54:20 +01:00
BTChip ae02835796 Fix derivePassphrase for Python 2 2017-01-25 19:25:01 +01:00
BTChip 53a76b055e Merge branch 'master' of https://github.com/LedgerHQ/blue-loader-python 2017-01-22 18:05:16 +01:00
BTChip 3738a758e3 Compatibility with Blue firmware 2.0 (production release) 2017-01-22 18:04:38 +01:00
BTChip 69c42dabfb Speedup 2017-01-22 18:03:41 +01:00
Nicolas Bacca e73a8a6389 Merge pull request #8 from xenithorb/master
Fix indent errors and print function for python3 builds
2017-01-09 20:40:51 +01:00
Michael Goodwin e728a7b57b Fix indent errors and print function for python3 builds 2017-01-09 14:34:06 -05:00
Nicolas Bacca 0e7d0378d1 Merge pull request #7 from ChaoticMind/py3
Fixup 78a3579: python3 compliance
2017-01-09 13:38:44 +01:00
BTChip 67a0a424a3 Add endorsement APIs 2017-01-09 13:36:38 +01:00
BTChip 2af852d628 Force to recompile secp256k1 for recent pip 2017-01-09 00:33:48 +01:00
BTChip c2feef8cb4 Add public key tweak for endorsement validation 2017-01-09 00:33:22 +01:00
Kevin Azzam 32c600e622 Fixup 78a3579: python3 compliance 2017-01-04 00:58:20 +01:00
BTChip c6e5961227 Update libudev package 2017-01-02 11:01:01 +01:00
BTChip c60427dc00 Update README with thotheolh suggestions 2017-01-02 10:35:46 +01:00
BTChip 43168d0292 Bump version 2016-12-08 00:11:36 +01:00
Nicolas Bacca 689ffb81ab Merge pull request #6 from NicolasDorier/patch-1
Encode correctly the passphrase in UTF8 NFKD
2016-12-07 15:10:04 -08:00
Nicolas Dorier a6c7c9ed7f Encode correctly the passphrase in UTF8 NFKD 2016-11-12 09:39:16 +09:00
BTChip ee42ea581f Add Pillow dependency 2016-10-13 14:27:34 +02:00
BTChip 78e64131d2 Add passphrase derivation utility 2016-10-11 18:23:12 +02:00
Nicolas Bacca 72b6a4378d Merge pull request #5 from cslashm/data-in-exception
add received data in CommException object
2016-10-11 14:55:17 +02:00
Cédric Mesnil 48f071cafa add received data in CommException object 2016-10-11 14:47:49 +02:00
BTChip 78a3579514 Add genuine device check script, support Secure Channel outgoing data, add an example with applications list 2016-10-11 13:13:20 +02:00
Nicolas Bacca cf74f4836c Merge pull request #4 from cslashm/py3
Python 3 port with keeping Python 2 compliance
2016-10-11 12:50:37 +02:00
Cédric Mesnil 0a4f7c4c91 Python 3 port with keeping Python 2 compliance
Add missing () around print arguments
  Replace <> with !=
  Fix bytes/str compliance

Status
  loadApp.py and deleteApp.py works with both python 2.7.12 and 3.5.2 on BlueLedger 1.2
  sign.py is untested because it requires en hsm access
2016-09-14 16:30:53 +02:00
BTChip dcf012d862 Merge branch 'master' of https://github.com/ledgerhq/blue-loader-python
j
2016-09-02 09:53:45 +02:00
BTChip b56ff7cbe1 Optional libsecp256k1 dependency, default to pure Python cryptographic API to help Windows users 2016-09-02 09:53:24 +02:00
Nicolas Bacca b5f80ac8f0 Merge pull request #3 from antonio-fr/master
Fix timeout from 7 hours to expected 20 seconds
2016-08-16 11:07:06 +02:00
a_ferron 58bc25a2bc Fix timeout from 7 hours to expected 20 seconds 2016-08-15 22:10:13 +02:00
BTChip 4eefb8a7f7 Update README with package on PyPI 2016-08-12 10:49:14 +02:00
44 changed files with 3798 additions and 527 deletions

View File

@ -1,13 +1,44 @@
# Python tools for Ledger Blue and Nano S
This package contains Python tools to communicate with Ledger Blue and Nano S and manage applications life cycle
This package contains Python tools to communicate with Ledger Blue and Nano S and manage applications life cycle.
The life cycle management requires [libsecp256k1](https://github.com/ludbb/secp256k1-py) Python bindings compiled with ECDH support. It is recommended to install this package in a [Virtual Environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/) in your native environment (not a Docker image) through
It is recommended to install this package in a [Virtual Environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/) in your native environment (not a Docker image) to avoid hidapi issues.
```
virtualenv ledger
source ledger/bin/activate
SECP_BUNDLED_EXPERIMENTAL=1 pip install secp256k1
pip install git+https://github.com/LedgerHQ/blue-loader-python.git
pip install ledgerblue
```
## Installation pre-requisites
* libudev-dev
* libusb-1.0-0-dev
* python-dev (python 2.7)
* virtualenv
This package can optionally work with [libsecp256k1](https://github.com/ludbb/secp256k1-py) Python bindings compiled with ECDH support. If you wish to enable libsecp256k1 bindings, make sure to install libsecp256k1 as follows:
```
SECP_BUNDLED_EXPERIMENTAL=1 pip --no-cache-dir install --no-binary secp256k1 secp256k1
```
## Giving permissions on udev
When running on Linux, make sure the following rules have been added to `/etc/udev/rules.d/`:
```
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000", MODE="0660", TAG+="uaccess", TAG+="udev-acl" OWNER="<UNIX username>"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001", MODE="0660", TAG+="uaccess", TAG+="udev-acl" OWNER="<UNIX username>"
```
## Target ID
Use the following Target IDs (--targetId option) when running commands directly:
* 0x31100002 on Nano S (until firmware 1.3.1, included)
* 0x31100003 on Nano S (after firmware 1.3.1)
* 0x31000002 on Blue

15
VERSIONING.md Normal file
View File

@ -0,0 +1,15 @@
# Versioning
The version for this repository is always the same as the version number for the
documentation. In `/doc/source/conf.py`, the `version` option will always be the
latest full version (latest version tag, not including pre-releases) and the
`release` option will be the same, except including pre-releases. As such, they
should always be bumped by the commit that was tagged with a version number.
The checklist for releasing a new version of this repository (and by extension,
of the documentation) is as follows:
1. Create a final commit that bumps the version number(s) in
`/doc/source/conf.py` and `/setup.py`.
2. Tag that commit with the appropriate version number.
3. Done! RTD should find the tag and build the docs automagically.

19
doc/Makefile Normal file
View File

@ -0,0 +1,19 @@
# Minimal Makefile for Sphinx documentation
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = BOLOSPythonLoader
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

26
doc/README.md Normal file
View File

@ -0,0 +1,26 @@
# BOLOS Python Tools Documentation
The latest version of this repository can be viewed, pre-built, here:
https://ledger.readthedocs.io/projects/blue-loader-python.
## Building
If you wish, you may install Sphinx and build this documentation into a
collection of HTML files yourself.
Firstly, make sure you have [pip
installed](https://pip.pypa.io/en/stable/installing/).
Secondly, install Sphinx and the Read the Docs theme:
```
pip install sphinx sphinx_rtd_theme
```
Finally, build:
```
make html
```
You will need internet access for intersphinx to work properly.

1
doc/requirements.txt Normal file
View File

@ -0,0 +1 @@
sphinx_argparse

View File

@ -0,0 +1,13 @@
/* override table width restrictions */
@media screen and (min-width: 767px) {
.wy-table-responsive table td {
/* !important prevents the common CSS stylesheets from overriding
this as on RTD they are loaded after this stylesheet */
white-space: normal !important;
}
.wy-table-responsive {
overflow: visible !important;
}
}

50
doc/source/conf.py Normal file
View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
# BOLOS Python Loader documentation build configuration file.
import os
import sys
sys.path.append(os.path.abspath('../../'))
def setup(app):
app.add_stylesheet('theme_overrides.css') # Override wide tables in RTD theme
# General Configuration
# =====================
extensions = []
source_suffix = ['.rst']
master_doc = 'index'
project = u'BOLOS Python Loader'
copyright = u'2017, Ledger Team'
author = u'Ledger Team'
version = u'0.1.15'
release = u'0.1.15'
pygments_style = 'sphinx'
# Options for HTML Output
# =======================
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
# sphinxarg
# =========
extensions += ['sphinxarg.ext']
# intersphinx
# ===========
extensions += ['sphinx.ext.intersphinx']
intersphinx_mapping = {
'ledger': ('https://ledger.readthedocs.io/en/2/', None)
}

14
doc/source/index.rst Normal file
View File

@ -0,0 +1,14 @@
BOLOS Python Loader
===================
The BOLOS Python loader is a Python library and collection of scripts for
interfacing with and managing BOLOS devices from a host computer. See the
`Python loader GitHub repository
<https://github.com/LedgerHQ/blue-loader-python>`_ for download and installation
instructions.
.. toctree::
:maxdepth: 1
scripts
script_reference

View File

@ -0,0 +1,212 @@
Script Reference
================
.. _checkGenuine.py:
checkGenuine.py
---------------
.. argparse::
:module: ledgerblue.checkGenuine
:func: get_argparser
:prog: python -m ledgerblue.checkGenuine
.. _deleteApp.py:
deleteApp.py
------------
.. argparse::
:module: ledgerblue.deleteApp
:func: get_argparser
:prog: python -m ledgerblue.deleteApp
.. _derivePassphrase.py:
derivePassphrase.py
-------------------
.. argparse::
:module: ledgerblue.derivePassphrase
:func: get_argparser
:prog: python -m ledgerblue.derivePassphrase
.. _endorsementSetupLedger.py:
endorsementSetupLedger.py
-------------------------
.. argparse::
:module: ledgerblue.endorsementSetupLedger
:func: get_argparser
:prog: python -m ledgerblue.endorsementSetupLedger
.. _endorsementSetup.py:
endorsementSetup.py
-------------------
.. argparse::
:module: ledgerblue.endorsementSetup
:func: get_argparser
:prog: python -m ledgerblue.endorsementSetup
.. _genCAPair.py:
genCAPair.py
------------
.. argparse::
:module: ledgerblue.genCAPair
:func: get_argparser
:prog: python -m ledgerblue.genCAPair
.. _hashApp.py:
hashApp.py
----------
.. argparse::
:module: ledgerblue.hashApp
:func: get_argparser
:prog: python -m ledgerblue.hashApp
.. _hostOnboard.py:
hostOnboard.py
--------------
.. argparse::
:module: ledgerblue.hostOnboard
:func: get_argparser
:prog: python -m ledgerblue.hostOnboard
.. _listApps.py:
listApps.py
-----------
.. argparse::
:module: ledgerblue.listApps
:func: get_argparser
:prog: python -m ledgerblue.listApps
.. _loadApp.py:
loadApp.py
----------
.. argparse::
:module: ledgerblue.loadApp
:func: get_argparser
:prog: python -m ledgerblue.loadApp
.. _loadMCU.py:
loadMCU.py
----------
.. argparse::
:module: ledgerblue.loadMCU
:func: get_argparser
:prog: python -m ledgerblue.loadMCU
.. _mcuBootloader.py:
mcuBootloader.py
----------------
.. argparse::
:module: ledgerblue.mcuBootloader
:func: get_argparser
:prog: python -m ledgerblue.mcuBootloader
.. _resetCustomCA.py:
resetCustomCA.py
----------------
.. argparse::
:module: ledgerblue.resetCustomCA
:func: get_argparser
:prog: python -m ledgerblue.resetCustomCA
.. _runApp.py:
runApp.py
---------
.. argparse::
:module: ledgerblue.runApp
:func: get_argparser
:prog: python -m ledgerblue.runApp
.. _runScript.py:
runScript.py
------------
.. argparse::
:module: ledgerblue.runScript
:func: get_argparser
:prog: python -m ledgerblue.runScript
.. _setupCustomCA.py:
setupCustomCA.py
----------------
.. argparse::
:module: ledgerblue.setupCustomCA
:func: get_argparser
:prog: python -m ledgerblue.setupCustomCA
.. _signApp.py:
signApp.py
----------
.. argparse::
:module: ledgerblue.signApp
:func: get_argparser
:prog: python -m ledgerblue.signApp
.. _updateFirmware.py:
updateFirmware.py
-----------------
.. argparse::
:module: ledgerblue.updateFirmware
:func: get_argparser
:prog: python -m ledgerblue.updateFirmware
.. _verifyApp.py:
verifyApp.py
------------
.. argparse::
:module: ledgerblue.verifyApp
:func: get_argparser
:prog: python -m ledgerblue.verifyApp
.. _verifyEndorsement1.py:
verifyEndorsement1.py
---------------------
.. argparse::
:module: ledgerblue.verifyEndorsement1
:func: get_argparser
:prog: python -m ledgerblue.verifyEndorsement1
.. _verifyEndorsement2.py:
verifyEndorsement2.py
---------------------
.. argparse::
:module: ledgerblue.verifyEndorsement2
:func: get_argparser
:prog: python -m ledgerblue.verifyEndorsement2

22
doc/source/scripts.rst Normal file
View File

@ -0,0 +1,22 @@
Scripts
=======
The Python loader includes a collection of useful scripts for managing BOLOS
devices. This section includes an overview of some of the most important scripts
and how they can be used.
In order to use any of these scripts, the device must be in the dashboard
application (no apps are open, the device should display a list of installed
apps).
Here is an example using the :ref:`deleteApp.py` script from the command-line:
.. code-block:: bash
python -m ledgerblue.deleteApp --targetId 0x31100002 --appName "Hello World"
The above command will delete the app named "Hello World" from the connected
Leger Nano S.
See the :doc:`script_reference` for the detailed documentation about each
script.

View File

@ -57,7 +57,7 @@ class BLEDongle(Dongle):
def exchange(self, apdu, timeout=20000):
if self.debug:
print "=> %s" % hexlify(apdu)
print("=> %s" % hexlify(apdu))
apdu = wrapCommandAPDU(0, apdu, DEFAULT_BLE_CHUNK, True)
offset = 0
while(offset < len(apdu)):
@ -78,8 +78,8 @@ class BLEDongle(Dongle):
sw = (result[swOffset] << 8) + result[swOffset + 1]
response = result[dataStart : dataLength + dataStart]
if self.debug:
print "<= %s%.2x" % (hexlify(response), sw)
if sw <> 0x9000:
print("<= %s%.2x" % (hexlify(response), sw))
if sw != 0x9000:
raise CommException("Invalid status %04x" % sw, sw)
return response

View File

@ -0,0 +1,223 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: BlueHSMServer.proto
import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
from google.protobuf import descriptor_pb2
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='BlueHSMServer.proto',
package='bluehsmserver',
serialized_pb=_b('\n\x13\x42lueHSMServer.proto\x12\rbluehsmserver\"7\n\tParameter\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\x12\r\n\x05local\x18\x03 \x01(\x08\"\xa1\x01\n\x07Request\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nparameters\x18\x02 \x01(\x0c\x12\x11\n\treference\x18\x03 \x01(\t\x12\x0b\n\x03\x65lf\x18\x04 \x01(\x0c\x12\r\n\x05\x63lose\x18\x05 \x01(\x08\x12\x12\n\nlargeStack\x18\x06 \x01(\x08\x12\x33\n\x11remote_parameters\x18\x07 \x03(\x0b\x32\x18.bluehsmserver.Parameter\"L\n\x08Response\x12\n\n\x02id\x18\x01 \x02(\t\x12\x10\n\x08response\x18\x02 \x01(\x0c\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x11\n\texception\x18\x04 \x01(\tB-\n\x1b\x63om.ledger.bluehsm.protobufB\x0c\x42lueHSMProtoH\x01')
)
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
_PARAMETER = _descriptor.Descriptor(
name='Parameter',
full_name='bluehsmserver.Parameter',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='name', full_name='bluehsmserver.Parameter.name', index=0,
number=1, type=9, cpp_type=9, label=2,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='alias', full_name='bluehsmserver.Parameter.alias', index=1,
number=2, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='local', full_name='bluehsmserver.Parameter.local', index=2,
number=3, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
],
extensions=[
],
nested_types=[],
enum_types=[
],
options=None,
is_extendable=False,
extension_ranges=[],
oneofs=[
],
serialized_start=38,
serialized_end=93,
)
_REQUEST = _descriptor.Descriptor(
name='Request',
full_name='bluehsmserver.Request',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='id', full_name='bluehsmserver.Request.id', index=0,
number=1, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='parameters', full_name='bluehsmserver.Request.parameters', index=1,
number=2, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='reference', full_name='bluehsmserver.Request.reference', index=2,
number=3, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='elf', full_name='bluehsmserver.Request.elf', index=3,
number=4, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='close', full_name='bluehsmserver.Request.close', index=4,
number=5, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='largeStack', full_name='bluehsmserver.Request.largeStack', index=5,
number=6, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='remote_parameters', full_name='bluehsmserver.Request.remote_parameters', index=6,
number=7, type=11, cpp_type=10, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
],
extensions=[
],
nested_types=[],
enum_types=[
],
options=None,
is_extendable=False,
extension_ranges=[],
oneofs=[
],
serialized_start=96,
serialized_end=257,
)
_RESPONSE = _descriptor.Descriptor(
name='Response',
full_name='bluehsmserver.Response',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='id', full_name='bluehsmserver.Response.id', index=0,
number=1, type=9, cpp_type=9, label=2,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='response', full_name='bluehsmserver.Response.response', index=1,
number=2, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='message', full_name='bluehsmserver.Response.message', index=2,
number=3, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
_descriptor.FieldDescriptor(
name='exception', full_name='bluehsmserver.Response.exception', index=3,
number=4, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None),
],
extensions=[
],
nested_types=[],
enum_types=[
],
options=None,
is_extendable=False,
extension_ranges=[],
oneofs=[
],
serialized_start=259,
serialized_end=335,
)
_REQUEST.fields_by_name['remote_parameters'].message_type = _PARAMETER
DESCRIPTOR.message_types_by_name['Parameter'] = _PARAMETER
DESCRIPTOR.message_types_by_name['Request'] = _REQUEST
DESCRIPTOR.message_types_by_name['Response'] = _RESPONSE
Parameter = _reflection.GeneratedProtocolMessageType('Parameter', (_message.Message,), dict(
DESCRIPTOR = _PARAMETER,
__module__ = 'BlueHSMServer_pb2'
# @@protoc_insertion_point(class_scope:bluehsmserver.Parameter)
))
_sym_db.RegisterMessage(Parameter)
Request = _reflection.GeneratedProtocolMessageType('Request', (_message.Message,), dict(
DESCRIPTOR = _REQUEST,
__module__ = 'BlueHSMServer_pb2'
# @@protoc_insertion_point(class_scope:bluehsmserver.Request)
))
_sym_db.RegisterMessage(Request)
Response = _reflection.GeneratedProtocolMessageType('Response', (_message.Message,), dict(
DESCRIPTOR = _RESPONSE,
__module__ = 'BlueHSMServer_pb2'
# @@protoc_insertion_point(class_scope:bluehsmserver.Response)
))
_sym_db.RegisterMessage(Response)
DESCRIPTOR.has_options = True
DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\033com.ledger.bluehsm.protobufB\014BlueHSMProtoH\001'))
# @@protoc_insertion_point(module_scope)

55
ledgerblue/Dongle.py Normal file
View File

@ -0,0 +1,55 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
from abc import ABCMeta, abstractmethod
from binascii import hexlify
import sys
TIMEOUT=20000
def hexstr(bstr):
if (sys.version_info.major == 3):
return hexlify(bstr).decode()
if (sys.version_info.major == 2):
return hexlify(bstr)
return "<undecoded APDU<"
class DongleWait(object):
__metaclass__ = ABCMeta
@abstractmethod
def waitFirstResponse(self, timeout):
pass
class Dongle(object):
__metaclass__ = ABCMeta
@abstractmethod
def exchange(self, apdu, timeout=TIMEOUT):
pass
@abstractmethod
def apduMaxDataSize(self):
pass
@abstractmethod
def close(self):
pass
def setWaitImpl(self, waitImpl):
self.waitImpl = waitImpl

168
ledgerblue/checkGenuine.py Normal file
View File

@ -0,0 +1,168 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="""Use attestation to determine if the device is a genuine Ledger
device.""")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--issuerKey", help="Issuer key (hex encoded, default is batch 1)")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
return parser
def auto_int(x):
return int(x, 0)
def getDeployedSecretV2(dongle, masterPrivate, targetId, issuerKey):
testMaster = PrivateKey(bytes(masterPrivate))
testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False))
targetid = bytearray(struct.pack('>I', targetId))
# identify
apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid
dongle.exchange(apdu)
# walk the chain
nonce = os.urandom(8)
apdu = bytearray([0xe0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce
auth_info = dongle.exchange(apdu)
batch_signer_serial = auth_info[0:4]
deviceNonce = auth_info[4:12]
# if not found, get another pair
#if cardKey != testMasterPublic:
# raise Exception("Invalid batch public key")
dataToSign = bytes(bytearray([0x01]) + testMasterPublic)
signature = testMaster.ecdsa_sign(bytes(dataToSign))
signature = testMaster.ecdsa_serialize(signature)
certificate = bytearray([len(testMasterPublic)]) + testMasterPublic + bytearray([len(signature)]) + signature
apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
# provide the ephemeral certificate
ephemeralPrivate = PrivateKey()
ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False))
dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic)
signature = testMaster.ecdsa_sign(bytes(dataToSign))
signature = testMaster.ecdsa_serialize(signature)
certificate = bytearray([len(ephemeralPublic)]) + ephemeralPublic + bytearray([len(signature)]) + signature
apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
# walk the device certificates to retrieve the public key to use for authentication
index = 0
last_pub_key = PublicKey(binascii.unhexlify(issuerKey), raw=True)
devicePublicKey = None
while True:
if index == 0:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000')))
elif index == 1:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000')))
else:
break
offset = 1
certificateHeader = certificate[offset : offset + certificate[offset-1]]
offset += certificate[offset-1] + 1
certificatePublicKey = certificate[offset : offset + certificate[offset-1]]
offset += certificate[offset-1] + 1
certificateSignatureArray = certificate[offset : offset + certificate[offset-1]]
certificateSignature = last_pub_key.ecdsa_deserialize(bytes(certificateSignatureArray))
# first cert contains a header field which holds the certificate's public key role
if index == 0:
devicePublicKey = certificatePublicKey
certificateSignedData = bytearray([0x02]) + certificateHeader + certificatePublicKey
# Could check if the device certificate is signed by the issuer public key
# ephemeral key certificate
else:
certificateSignedData = bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey
if not last_pub_key.ecdsa_verify(bytes(certificateSignedData), certificateSignature):
return None
last_pub_key = PublicKey(bytes(certificatePublicKey), raw=True)
index = index + 1
# Commit device ECDH channel
dongle.exchange(bytearray.fromhex('E053000000'))
secret = last_pub_key.ecdh(binascii.unhexlify(ephemeralPrivate.serialize()))
if targetId&0xF == 0x2:
return secret[0:16]
elif targetId&0xF == 0x3:
ret = {}
ret['ecdh_secret'] = secret
ret['devicePublicKey'] = devicePublicKey
return ret
if __name__ == '__main__':
from .ecWrapper import PrivateKey, PublicKey
from .comm import getDongle
from .commException import CommException
from .hexLoader import HexLoader
import struct
import os
import binascii
args = get_argparser().parse_args()
if args.targetId == None:
args.targetId = 0x31000002
if args.issuerKey == None:
args.issuerKey = "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f818057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609"
privateKey = PrivateKey()
publicKey = str(privateKey.pubkey.serialize(compressed=False))
args.rootPrivateKey = privateKey.serialize()
genuine = False
ui = False
customCA = False
dongle = getDongle(args.apdu)
version = None
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId, args.issuerKey)
if secret != None:
try:
loader = HexLoader(dongle, 0xe0, True, secret)
version = loader.getVersion()
genuine = True
apps = loader.listApp()
while len(apps) != 0:
for app in apps:
if (app['flags'] & 0x08):
ui = True
if (app['flags'] & 0x400):
customCA = True
apps = loader.listApp(False)
except:
genuine = False
if genuine:
if ui:
print ("WARNING : Product is genuine but has a UI application loaded")
if customCA:
print ("WARNING : Product is genuine but has a Custom CA loaded")
if not ui and not customCA:
print ("Product is genuine")
print ("SE Version " + version['osVersion'])
print ("MCU Version " + version['mcuVersion'])
if 'mcuHash' in version:
print ("MCU Hash " + binascii.hexlify(version['mcuHash']).decode('ascii'))
else:
print ("Product is NOT genuine")

View File

@ -20,38 +20,38 @@
from abc import ABCMeta, abstractmethod
from .commException import CommException
from .ledgerWrapper import wrapCommandAPDU, unwrapResponseAPDU
from .Dongle import *
from binascii import hexlify
import hid
import time
import os
import sys
from .commU2F import getDongle as getDongleU2F
from .commHTTP import getDongle as getDongleHTTP
import hid
try:
from smartcard.Exceptions import NoCardException
from smartcard.System import readers
from smartcard.util import toHexString, toBytes
SCARD = True
except ImportError:
SCARD = False
class DongleWait(object):
__metaclass__ = ABCMeta
@abstractmethod
def waitFirstResponse(self, timeout):
pass
class Dongle(object):
__metaclass__ = ABCMeta
@abstractmethod
def exchange(self, apdu, timeout=20000):
pass
@abstractmethod
def close(self):
pass
def setWaitImpl(self, waitImpl):
self.waitImpl = waitImpl
APDUGEN=None
if "APDUGEN" in os.environ and len(os.environ["APDUGEN"]) != 0:
APDUGEN=os.environ["APDUGEN"]
# Force use of U2F if required
U2FKEY=None
if "U2FKEY" in os.environ and len(os.environ["U2FKEY"]) != 0:
U2FKEY=os.environ["U2FKEY"]
# Force use of MCUPROXY if required
MCUPROXY=None
if "MCUPROXY" in os.environ and len(os.environ["MCUPROXY"]) != 0:
MCUPROXY=os.environ["MCUPROXY"]
# Force use of MCUPROXY if required
PCSC=None
if "PCSC" in os.environ and len(os.environ["PCSC"]) != 0:
PCSC=os.environ["PCSC"]
if PCSC:
try:
from smartcard.Exceptions import NoCardException
from smartcard.System import readers
from smartcard.util import toHexString, toBytes
except ImportError:
PCSC = False
class HIDDongleHIDAPI(Dongle, DongleWait):
@ -62,20 +62,25 @@ class HIDDongleHIDAPI(Dongle, DongleWait):
self.waitImpl = self
self.opened = True
def exchange(self, apdu, timeout=20000):
def exchange(self, apdu, timeout=TIMEOUT):
if APDUGEN:
print("%s" % hexstr(apdu))
return
if self.debug:
print "=> %s" % hexlify(apdu)
print("HID => %s" % hexstr(apdu))
if self.ledger:
apdu = wrapCommandAPDU(0x0101, apdu, 64)
padSize = len(apdu) % 64
tmp = apdu
if padSize <> 0:
if padSize != 0:
tmp.extend([0] * (64 - padSize))
offset = 0
while(offset <> len(tmp)):
while(offset != len(tmp)):
data = tmp[offset:offset + 64]
data = bytearray([0]) + data
self.device.write(data)
if self.device.write(data) < 0:
raise BaseException("Error while writing")
offset += 64
dataLength = 0
dataStart = 2
@ -114,9 +119,14 @@ class HIDDongleHIDAPI(Dongle, DongleWait):
sw = (result[swOffset] << 8) + result[swOffset + 1]
response = result[dataStart : dataLength + dataStart]
if self.debug:
print "<= %s%.2x" % (hexlify(response), sw)
if sw <> 0x9000:
raise CommException("Invalid status %04x" % sw, sw)
print("HID <= %s%.2x" % (hexstr(response), sw))
if sw != 0x9000:
possibleCause = "Unknown reason"
if sw == 0x6982:
possibleCause = "Have you uninstalled the existing CA with resetCustomCA first?"
if sw == 0x6484:
possibleCause = "Are you using the correct targetId?"
raise CommException("Invalid status %04x (%s)" % (sw, possibleCause), sw, response)
return response
def waitFirstResponse(self, timeout):
@ -127,9 +137,12 @@ class HIDDongleHIDAPI(Dongle, DongleWait):
if not len(data):
if time.time() - start > timeout:
raise CommException("Timeout")
time.sleep(0.02)
time.sleep(0.0001)
return bytearray(data)
def apduMaxDataSize(self):
return 255
def close(self):
if self.opened:
try:
@ -146,15 +159,15 @@ class DongleSmartcard(Dongle):
self.waitImpl = self
self.opened = True
def exchange(self, apdu, timeout=20000):
def exchange(self, apdu, timeout=TIMEOUT):
if self.debug:
print "=> %s" % hexlify(apdu)
print("SC => %s" % hexstr(apdu))
response, sw1, sw2 = self.device.transmit(toBytes(hexlify(apdu)))
sw = (sw1 << 8) | sw2
if self.debug:
print "<= %s%.2x" % (toHexString(response).replace(" ", ""), sw)
if sw <> 0x9000:
raise CommException("Invalid status %04x" % sw, sw)
print("SC <= %s%.2x" % (hexstr(response).replace(" ", ""), sw))
if sw != 0x9000:
raise CommException("Invalid status %04x" % sw, sw, bytearray(response))
return bytearray(response)
def close(self):
@ -166,24 +179,32 @@ class DongleSmartcard(Dongle):
self.opened = False
def getDongle(debug=False, selectCommand=None):
if APDUGEN:
return HIDDongleHIDAPI(None, True, debug)
if not U2FKEY is None:
return getDongleU2F(scrambleKey=U2FKEY, debug=debug)
if MCUPROXY is not None:
return getDongleHTTP(remote_host=MCUPROXY, debug=debug)
dev = None
hidDevicePath = None
ledger = True
for hidDevice in hid.enumerate(0, 0):
if hidDevice['vendor_id'] == 0x2c97:
hidDevicePath = hidDevice['path']
if ('interface_number' in hidDevice and hidDevice['interface_number'] == 0) or ('usage_page' in hidDevice and hidDevice['usage_page'] == 0xffa0):
hidDevicePath = hidDevice['path']
if hidDevicePath is not None:
dev = hid.device()
dev.open_path(hidDevicePath)
dev.set_nonblocking(True)
return HIDDongleHIDAPI(dev, ledger, debug)
if SCARD:
if PCSC:
connection = None
for reader in readers():
try:
connection = reader.createConnection()
connection.connect()
if selectCommand <> None:
if selectCommand != None:
response, sw1, sw2 = connection.transmit(toBytes("00A4040010FF4C4547522E57414C5430312E493031"))
sw = (sw1 << 8) | sw2
if sw == 0x9000:

View File

@ -19,9 +19,10 @@
class CommException(Exception):
def __init__(self, message, sw=0x6f00):
def __init__(self, message, sw=0x6f00, data=None):
self.message = message
self.sw = sw
self.data = data
def __str__(self):
buf = "Exception : " + self.message

112
ledgerblue/commHTTP.py Normal file
View File

@ -0,0 +1,112 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
from abc import ABCMeta, abstractmethod
from .commException import CommException
from .ledgerWrapper import wrapCommandAPDU, unwrapResponseAPDU
from binascii import hexlify
import time
import os
import sys
import requests
import json
def hexstr(bstr):
if (sys.version_info.major == 3):
return hexlify(bstr).decode()
if (sys.version_info.major == 2):
return hexlify(bstr)
return "<undecoded APDU<"
class HTTPProxy(object):
def __init__(self, remote_host="localhost:8081", debug=False):
self.remote_host = "http://" + remote_host
self.debug = debug
def exchange(self, apdu):
if self.debug:
print("=> %s" % hexstr(apdu))
try:
ret = requests.post(self.remote_host + "/send_apdu", params={"data": hexstr(apdu)})
while True:
ret = requests.post(self.remote_host + "/fetch_apdu")
if ret.text != "no response apdu yet":
print("<= %s" % ret.text)
break
else:
time.sleep(0.1)
return bytearray(str(ret.text).decode("hex"))
except Exception as e:
print(e)
def exchange_seph_event(self, event):
if self.debug >= 3:
print("=> %s" % hexstr(event))
try:
ret = requests.post(self.remote_host + "/send_seph_event", params={"data": event.encode("hex")})
return ret.text
except Exception as e:
print(e)
def poll_status(self):
if self.debug >= 5:
print("=> Waiting for a status")
try:
while True:
ret = requests.post(self.remote_host + "/fetch_status")
if ret.text != "no status yet":
break
else:
time.sleep(0.05)
return bytearray(str(ret.text).decode("hex"))
except Exception as e:
print(e)
def reset(self):
if self.debug:
print("=> Reset")
try:
ret = requests.post(self.remote_host + "/reset")
except Exception as e:
print(e)
def getDongle(remote_host="localhost", debug=False):
return HTTPProxy(remote_host, debug)

333
ledgerblue/commU2F.py Normal file
View File

@ -0,0 +1,333 @@
# Copyright (c) 2013 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import os
import traceback
from abc import ABCMeta, abstractmethod
from .ledgerWrapper import wrapCommandAPDU, unwrapResponseAPDU
from binascii import hexlify
from .Dongle import *
import binascii
import time
import sys
import hid
from u2flib_host.device import U2FDevice
from u2flib_host.yubicommon.compat import byte2int, int2byte
from u2flib_host.constants import INS_ENROLL, INS_SIGN
from u2flib_host import u2f, exc
from u2flib_host.utils import websafe_decode, websafe_encode
from hashlib import sha256
from .commException import CommException
TIMEOUT=30000
DEVICES = [
(0x1050, 0x0200), # Gnubby
(0x1050, 0x0113), # YubiKey NEO U2F
(0x1050, 0x0114), # YubiKey NEO OTP+U2F
(0x1050, 0x0115), # YubiKey NEO U2F+CCID
(0x1050, 0x0116), # YubiKey NEO OTP+U2F+CCID
(0x1050, 0x0120), # Security Key by Yubico
(0x1050, 0x0410), # YubiKey Plus
(0x1050, 0x0402), # YubiKey 4 U2F
(0x1050, 0x0403), # YubiKey 4 OTP+U2F
(0x1050, 0x0406), # YubiKey 4 U2F+CCID
(0x1050, 0x0407), # YubiKey 4 OTP+U2F+CCID
(0x2581, 0xf1d0), # Plug-Up U2F Security Key
(0x2581, 0xf1d1), # Ledger Production U2F Dongle
(0x2c97, 0x0000), # Ledger Blue
(0x2c97, 0x0001), # Ledger Nano S
(0x2c97, 0x0002), # Ledger Aramis
(0x2c97, 0x0003), # Ledger HW2
(0x2c97, 0x0004), # Ledger Blend
(0x2c97, 0xf1d0), # Plug-Up U2F Security Key
]
HID_RPT_SIZE = 64
TYPE_INIT = 0x80
U2F_VENDOR_FIRST = 0x40
CMD_INIT = 0x06
CMD_WINK = 0x08
CMD_APDU = 0x03
U2FHID_YUBIKEY_DEVICE_CONFIG = U2F_VENDOR_FIRST
STAT_ERR = 0xbf
def _read_timeout(dev, size, timeout=TIMEOUT):
if (timeout > 0):
timeout += time.time()
while timeout == 0 or time.time() < timeout:
resp = dev.read(size)
if resp:
return resp
time.sleep(0.01)
return []
class U2FHIDError(Exception):
def __init__(self, code):
super(Exception, self).__init__("U2FHIDError: 0x%02x" % code)
self.code = code
class HIDDevice(U2FDevice):
"""
U2FDevice implementation using the HID transport.
"""
def __init__(self, path):
self.path = path
self.cid = b"\xff\xff\xff\xff"
def open(self):
self.handle = hid.device()
self.handle.open_path(self.path)
self.handle.set_nonblocking(True)
self.init()
def close(self):
if hasattr(self, 'handle'):
self.handle.close()
del self.handle
def init(self):
nonce = os.urandom(8)
resp = self.call(CMD_INIT, nonce)
while resp[:8] != nonce:
print("Wrong nonce, read again...")
resp = self._read_resp(self.cid, CMD_INIT)
self.cid = resp[8:12]
def set_mode(self, mode):
data = mode + b"\x0f\x00\x00"
self.call(U2FHID_YUBIKEY_DEVICE_CONFIG, data)
def _do_send_apdu(self, apdu_data):
return self.call(CMD_APDU, apdu_data)
def wink(self):
self.call(CMD_WINK)
def _send_req(self, cid, cmd, data):
size = len(data)
bc_l = int2byte(size & 0xff)
bc_h = int2byte(size >> 8 & 0xff)
payload = cid + int2byte(TYPE_INIT | cmd) + bc_h + bc_l + \
data[:HID_RPT_SIZE - 7]
payload += b'\0' * (HID_RPT_SIZE - len(payload))
if self.handle.write([0] + [byte2int(c) for c in payload]) < 0:
raise exc.DeviceError("Cannot write to device!")
data = data[HID_RPT_SIZE - 7:]
seq = 0
while len(data) > 0:
payload = cid + int2byte(0x7f & seq) + data[:HID_RPT_SIZE - 5]
payload += b'\0' * (HID_RPT_SIZE - len(payload))
if self.handle.write([0] + [byte2int(c) for c in payload]) < 0:
raise exc.DeviceError("Cannot write to device!")
data = data[HID_RPT_SIZE - 5:]
seq += 1
def _read_resp(self, cid, cmd):
resp = b'.'
header = cid + int2byte(TYPE_INIT | cmd)
while resp and resp[:5] != header:
# allow for timeout
resp_vals = _read_timeout(self.handle, HID_RPT_SIZE)
resp = b''.join(int2byte(v) for v in resp_vals)
if resp[:5] == cid + int2byte(STAT_ERR):
raise U2FHIDError(byte2int(resp[7]))
if not resp:
raise exc.DeviceError("Invalid response from device!")
data_len = (byte2int(resp[5]) << 8) + byte2int(resp[6])
data = resp[7:min(7 + data_len, HID_RPT_SIZE)]
data_len -= len(data)
seq = 0
while data_len > 0:
resp_vals = _read_timeout(self.handle, HID_RPT_SIZE)
resp = b''.join(int2byte(v) for v in resp_vals)
if resp[:4] != cid:
raise exc.DeviceError("Wrong CID from device!")
if byte2int(resp[4:5]) != seq & 0x7f:
raise exc.DeviceError("Wrong SEQ from device!")
seq += 1
new_data = resp[5:min(5 + data_len, HID_RPT_SIZE)]
data_len -= len(new_data)
data += new_data
return data
def call(self, cmd, data=b''):
if isinstance(data, int):
data = int2byte(data)
self._send_req(self.cid, cmd, data)
return self._read_resp(self.cid, cmd)
class U2FTunnelDongle(Dongle, DongleWait):
def __init__(self, device, scrambleKey="", ledger=False, debug=False):
self.device = device
self.scrambleKey = scrambleKey
self.ledger = ledger
self.debug = debug
self.waitImpl = self
self.opened = True
self.device.open()
def exchange(self, apdu, timeout=TIMEOUT):
if self.debug:
print("U2F => %s" % hexstr(apdu))
if (len(apdu)>=256):
raise CommException("Too long APDU to transport")
# wrap apdu
i=0
keyHandle = ""
while i < len(apdu):
val = apdu[i:i+1]
if len(self.scrambleKey) > 0:
val = chr(ord(val) ^ ord(self.scrambleKey[i % len(self.scrambleKey)]))
keyHandle += val
i+=1
client_param = sha256("u2f_tunnel".encode('utf8')).digest()
app_param = sha256("u2f_tunnel".encode('utf8')).digest()
request = client_param + app_param + int2byte(len(keyHandle)) + keyHandle
#p1 = 0x07 if check_only else 0x03
p1 = 0x03
p2 = 0
response = self.device.send_apdu(INS_SIGN, p1, p2, request)
if self.debug:
print("U2F <= %s%.2x" % (hexstr(response), 0x9000))
# check replied status words of the command (within the APDU tunnel)
if hexstr(response[-2:]) != "9000":
raise CommException("Invalid status words received: " + hexstr(response[-2:]));
# api expect a byte array, remove the appended status words
return bytearray(response[:-2])
def apduMaxDataSize(self):
return 256-5
def close(self):
self.device.close()
def waitFirstResponse(self, timeout):
raise CommException("Invalid use")
def getDongles(dev_class=None, scrambleKey="", debug=False):
dev_class = dev_class or HIDDevice
devices = []
for d in hid.enumerate(0, 0):
usage_page = d['usage_page']
if usage_page == 0xf1d0 and d['usage'] == 1:
devices.append(U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug))
# Usage page doesn't work on Linux
# well known devices
elif (d['vendor_id'], d['product_id']) in DEVICES:
device = HIDDevice(d['path'])
try:
device.open()
device.close()
devices.append(U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug))
except (exc.DeviceError, IOError, OSError):
pass
# unknown devices
else:
device = HIDDevice(d['path'])
try:
device.open()
# try a ping command to ensure a FIDO device, else timeout (BEST here, modulate the timeout, 2 seconds is way too big)
device.ping()
device.close()
devices.append(U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug))
except (exc.DeviceError, IOError, OSError):
pass
return devices
def getDongle(path=None, dev_class=None, scrambleKey="", debug=False):
# if path is none, then use the first device
dev_class = dev_class or HIDDevice
devices = []
for d in hid.enumerate(0, 0):
if path is None or d['path'] == path:
usage_page = d['usage_page']
if usage_page == 0xf1d0 and d['usage'] == 1:
return U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug)
# Usage page doesn't work on Linux
# well known devices
elif (d['vendor_id'], d['product_id']) in DEVICES and ('interface_number' not in d or d['interface_number'] == 1):
#print d
device = HIDDevice(d['path'])
try:
device.open()
device.close()
return U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug)
except (exc.DeviceError, IOError, OSError):
traceback.print_exc()
pass
# unknown devices
# else:
# device = HIDDevice(d['path'])
# try:
# device.open()
# # try a ping command to ensure a FIDO device, else timeout (BEST here, modulate the timeout, 2 seconds is way too big)
# device.ping()
# device.close()
# return U2FTunnelDongle(dev_class(d['path']),scrambleKey, debug=debug)
# except (exc.DeviceError, IOError, OSError):
# traceback.print_exc()
# pass
raise CommException("No dongle found")

View File

@ -17,39 +17,67 @@
********************************************************************************
"""
from secp256k1 import PrivateKey
from .comm import getDongle
from .deployed import getDeployedSecretV1, getDeployedSecretV2
from .hexLoader import HexLoader
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Delete the app with the specified name.")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--appName", help="The name of the application to delete")
parser.add_argument("--appHash", help="Set the application hash")
parser.add_argument("--rootPrivateKey", help="A private key used to establish a Secure Channel (hex encoded)")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true')
return parser
def auto_int(x):
return int(x, 0)
return int(x, 0)
parser = argparse.ArgumentParser()
parser.add_argument("--targetId", help="Set the chip target ID", type=auto_int)
parser.add_argument("--appName", help="Set the application name")
parser.add_argument("--rootPrivateKey", help="Set the root private key")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true')
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .deployed import getDeployedSecretV1, getDeployedSecretV2
from .hexLoader import HexLoader
import binascii
import sys
args = parser.parse_args()
args = get_argparser().parse_args()
if args.appName == None:
raise Exception("Missing appName")
if args.targetId == None:
args.targetId = 0x31000002
if args.rootPrivateKey == None:
privateKey = PrivateKey()
publicKey = str(privateKey.pubkey.serialize(compressed=False)).encode('hex')
print "Generated random root public key : " + publicKey
args.rootPrivateKey = privateKey.serialize().encode('ascii')
if args.appName == None and args.appHash == None:
raise Exception("Missing appName or appHash")
if args.appName != None and args.appHash != None:
raise Exception("Set either appName or appHash")
dongle = getDongle(args.apdu)
if args.appName != None:
if (sys.version_info.major == 3):
args.appName = bytes(args.appName,'ascii')
if (sys.version_info.major == 2):
args.appName = bytes(args.appName)
if args.deployLegacy:
secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
else:
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
loader = HexLoader(dongle, 0xe0, True, secret)
loader.deleteApp(args.appName)
if args.appHash != None:
if (sys.version_info.major == 3):
args.appHash = bytes(args.appHash,'ascii')
if (sys.version_info.major == 2):
args.appHash = bytes(args.appHash)
args.appHash = bytearray.fromhex(args.appHash)
if args.targetId == None:
args.targetId = 0x31000002
if args.rootPrivateKey == None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
dongle = getDongle(args.apdu)
if args.deployLegacy:
secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
else:
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
loader = HexLoader(dongle, 0xe0, True, secret)
if args.appName != None:
loader.deleteApp(args.appName)
if args.appHash != None:
loader.deleteAppByHash(args.appHash)

View File

@ -17,18 +17,22 @@
********************************************************************************
"""
from secp256k1 import PrivateKey, PublicKey
from .ecWrapper import PrivateKey, PublicKey
import os
import sys
import struct
from .hexParser import IntelHexParser
from .hexLoader import HexLoader
import binascii
def getDeployedSecretV1(dongle, masterPrivate, targetid):
testMaster = PrivateKey(bytes(masterPrivate))
testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False))
targetid = bytearray(struct.pack('>I', targetid))
if targetId&0xF != 0x1:
raise BaseException("Target ID does not support SCP V1")
# identify
apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid
dongle.exchange(apdu)
@ -38,13 +42,13 @@ def getDeployedSecretV1(dongle, masterPrivate, targetid):
cardKey = batch_info[5:5 + batch_info[4]]
# if not found, get another pair
#if cardKey <> testMasterPublic:
#if cardKey != testMasterPublic:
# raise Exception("Invalid batch public key")
# provide the ephemeral certificate
ephemeralPrivate = PrivateKey()
ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False))
print "Using ephemeral key " + str(ephemeralPublic).encode('hex')
print("Using ephemeral key %s" %binascii.hexlify(ephemeralPublic))
signature = testMaster.ecdsa_sign(bytes(ephemeralPublic))
signature = testMaster.ecdsa_serialize(signature)
certificate = bytearray([len(ephemeralPublic)]) + ephemeralPublic + bytearray([len(signature)]) + signature
@ -63,7 +67,7 @@ def getDeployedSecretV1(dongle, masterPrivate, targetid):
if not last_pub_key.ecdsa_verify(bytes(certificatePublic), certificateSignature):
if index == 0:
# Not an error if loading from user key
print "Broken certificate chain - loading from user key"
print("Broken certificate chain - loading from user key")
else:
raise Exception("Broken certificate chain")
last_pub_key = PublicKey(bytes(certificatePublic), raw=True)
@ -72,12 +76,15 @@ def getDeployedSecretV1(dongle, masterPrivate, targetid):
# Commit device ECDH channel
dongle.exchange(bytearray.fromhex('E053000000'))
secret = last_pub_key.ecdh(bytes(ephemeralPrivate.serialize().decode('hex')))
return str(secret[0:16])
return secret[0:16]
def getDeployedSecretV2(dongle, masterPrivate, targetid):
def getDeployedSecretV2(dongle, masterPrivate, targetId, signerCertChain=None, ecdh_secret_format=None):
testMaster = PrivateKey(bytes(masterPrivate))
testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False))
targetid = bytearray(struct.pack('>I', targetid))
targetid = bytearray(struct.pack('>I', targetId))
if targetId&0xF == 0x1:
raise BaseException("Target ID does not support SCP V2")
# identify
apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid
@ -91,21 +98,26 @@ def getDeployedSecretV2(dongle, masterPrivate, targetid):
deviceNonce = auth_info[4:12]
# if not found, get another pair
#if cardKey <> testMasterPublic:
#if cardKey != testMasterPublic:
# raise Exception("Invalid batch public key")
print "Using test master key " + str(testMasterPublic).encode('hex')
dataToSign = bytes(bytearray([0x01]) + testMasterPublic)
signature = testMaster.ecdsa_sign(bytes(dataToSign))
signature = testMaster.ecdsa_serialize(signature)
certificate = bytearray([len(testMasterPublic)]) + testMasterPublic + bytearray([len(signature)]) + signature
apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
if (signerCertChain):
for cert in signerCertChain:
apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(cert)]) + cert
dongle.exchange(apdu)
else:
print("Using test master key %s " % binascii.hexlify(testMasterPublic))
dataToSign = bytes(bytearray([0x01]) + testMasterPublic)
signature = testMaster.ecdsa_sign(bytes(dataToSign))
signature = testMaster.ecdsa_serialize(signature)
certificate = bytearray([len(testMasterPublic)]) + testMasterPublic + bytearray([len(signature)]) + signature
apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
# provide the ephemeral certificate
ephemeralPrivate = PrivateKey()
ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False))
print "Using ephemeral key " + str(ephemeralPublic).encode('hex')
print("Using ephemeral key %s" %binascii.hexlify(ephemeralPublic))
dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic)
signature = testMaster.ecdsa_sign(bytes(dataToSign))
signature = testMaster.ecdsa_serialize(signature)
@ -115,40 +127,50 @@ def getDeployedSecretV2(dongle, masterPrivate, targetid):
# walk the device certificates to retrieve the public key to use for authentication
index = 0
last_pub_key = PublicKey(bytes(testMasterPublic), raw=True)
last_dev_pub_key = PublicKey(bytes(testMasterPublic), raw=True)
devicePublicKey = None
while True:
if index == 0:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000')))
elif index == 1:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000')))
else:
break
break
if len(certificate) == 0:
break
offset = 1
offset = 1
certificateHeader = certificate[offset : offset + certificate[offset-1]]
offset += certificate[offset-1] + 1
certificatePublicKey = certificate[offset : offset + certificate[offset-1]]
offset += certificate[offset-1] + 1
certificateSignatureArray = certificate[offset : offset + certificate[offset-1]]
certificateSignature = last_pub_key.ecdsa_deserialize(bytes(certificateSignatureArray))
certificateSignature = last_dev_pub_key.ecdsa_deserialize(bytes(certificateSignatureArray))
# first cert contains a header field which holds the certificate's public key role
if index == 0:
devicePublicKey = certificatePublicKey
certificateSignedData = bytearray([0x02]) + certificateHeader + certificatePublicKey
# Could check if the device certificate is signed by the issuer public key
# ephemeral key certificate
else:
certificateSignedData = bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey
if not last_pub_key.ecdsa_verify(bytes(certificateSignedData), certificateSignature):
if not last_dev_pub_key.ecdsa_verify(bytes(certificateSignedData), certificateSignature):
if index == 0:
# Not an error if loading from user key
print "Broken certificate chain - loading from user key"
print("Broken certificate chain - loading from user key")
else:
raise Exception("Broken certificate chain")
last_pub_key = PublicKey(bytes(certificatePublicKey), raw=True)
last_dev_pub_key = PublicKey(bytes(certificatePublicKey), raw=True)
index = index + 1
# Commit device ECDH channel
dongle.exchange(bytearray.fromhex('E053000000'))
secret = last_pub_key.ecdh(bytes(ephemeralPrivate.serialize().decode('hex')))
return str(secret[0:16])
secret = last_dev_pub_key.ecdh(binascii.unhexlify(ephemeralPrivate.serialize()))
#forced to specific version
if ecdh_secret_format==1 or targetId&0xF == 0x2:
return secret[0:16]
elif targetId&0xF == 0x3:
ret = {}
ret['ecdh_secret'] = secret
ret['devicePublicKey'] = devicePublicKey
return ret

View File

@ -0,0 +1,51 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Set a BIP 39 passphrase on the device.")
parser.add_argument("--persistent", help="""Persist passphrase as secondary PIN (otherwise, it's set as a temporary
passphrase)""", action='store_true')
return parser
def auto_int(x):
return int(x, 0)
if __name__ == '__main__':
from .comm import getDongle
import getpass
import unicodedata
import sys
args = get_argparser().parse_args()
dongle = getDongle(False)
passphrase = getpass.getpass("Enter BIP39 passphrase : ")
if isinstance(passphrase, bytes):
passphrase = passphrase.decode(sys.stdin.encoding)
if len(passphrase) != 0:
if args.persistent:
p1 = 0x02
else:
p1 = 0x01
passphrase = unicodedata.normalize('NFKD', passphrase)
apdu = bytearray([0xE0, 0xD0, p1, 0x00, len(passphrase)]) + bytearray(passphrase, 'utf8')
dongle.exchange(apdu, timeout=300)

144
ledgerblue/ecWrapper.py Normal file
View File

@ -0,0 +1,144 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import hashlib
try:
import secp256k1
USE_SECP = secp256k1.HAS_ECDH
except ImportError:
USE_SECP = False
if not USE_SECP:
import ecpy
from builtins import int
from ecpy.curves import Curve, Point
from ecpy.keys import ECPublicKey, ECPrivateKey
from ecpy.ecdsa import ECDSA
CURVE_SECP256K1 = Curve.get_curve('secp256k1')
SIGNER = ECDSA()
class PublicKey(object):
def __init__(self, pubkey=None, raw=False, flags=None, ctx=None):
if USE_SECP:
if flags == None:
flags = secp256k1.FLAG_VERIFY
self.obj = secp256k1.PublicKey(pubkey, raw, flags, ctx)
else:
if not raw:
raise Exception("Non raw init unsupported")
pubkey = pubkey[1:]
x = int.from_bytes(pubkey[0:32], 'big')
y = int.from_bytes(pubkey[32:], 'big')
self.obj = ECPublicKey(Point(x, y, CURVE_SECP256K1))
def ecdsa_deserialize(self, ser_sig):
if USE_SECP:
return self.obj.ecdsa_deserialize(ser_sig)
else:
return ser_sig
def serialize(self, compressed=True):
if USE_SECP:
return self.obj.serialize(compressed)
else:
if not compressed:
out = b"\x04"
out += self.obj.W.x.to_bytes(32, 'big')
out += self.obj.W.y.to_bytes(32, 'big')
else:
out = b"\x03" if ((self.obj.W.y & 1) != 0) else "\x02"
out += self.obj.W.x.to_bytes(32, 'big')
return out
def ecdh(self, scalar):
if USE_SECP:
return self.obj.ecdh(scalar)
else:
scalar = int.from_bytes(scalar, 'big')
point = self.obj.W * scalar
# libsecp256k1 style secret
out = b"\x03" if ((point.y & 1) != 0) else b"\x02"
out += point.x.to_bytes(32, 'big')
hash = hashlib.sha256()
hash.update(out)
return hash.digest()
def tweak_add(self, scalar):
if USE_SECP:
self.obj = self.obj.tweak_add(scalar)
else:
scalar = int.from_bytes(scalar, 'big')
privKey = ECPrivateKey(scalar, CURVE_SECP256K1)
self.obj = ECPublicKey(self.obj.W + privKey.get_public_key().W)
def ecdsa_verify(self, msg, raw_sig, raw=False, digest=hashlib.sha256):
if USE_SECP:
return self.obj.ecdsa_verify(msg, raw_sig, raw, digest)
else:
if not raw:
h = digest()
h.update(msg)
msg = h.digest()
raw_sig = bytearray(raw_sig)
return SIGNER.verify(msg, raw_sig, self.obj)
class PrivateKey(object):
def __init__(self, privkey=None, raw=True, flags=None, ctx=None):
if USE_SECP:
if flags == None:
flags = secp256k1.ALL_FLAGS
self.obj = secp256k1.PrivateKey(privkey, raw, flags, ctx)
self.pubkey = self.obj.pubkey
else:
if not raw:
raise Exception("Non raw init unsupported")
if privkey == None:
privkey = ecpy.ecrand.rnd(CURVE_SECP256K1.order)
else:
privkey = int.from_bytes(privkey,'big')
self.obj = ECPrivateKey(privkey, CURVE_SECP256K1)
pubkey = self.obj.get_public_key().W
out = b"\x04"
out += pubkey.x.to_bytes(32, 'big')
out += pubkey.y.to_bytes(32, 'big')
self.pubkey = PublicKey(out, raw=True)
def serialize(self):
if USE_SECP:
return self.obj.serialize()
else:
return "%.64x"%self.obj.d
def ecdsa_serialize(self, raw_sig):
if USE_SECP:
return self.obj.ecdsa_serialize(raw_sig)
else:
return raw_sig
def ecdsa_sign(self, msg, raw=False, digest=hashlib.sha256):
if USE_SECP:
return self.obj.ecdsa_sign(msg, raw, digest)
else:
if not raw:
h = digest()
h.update(msg)
msg = h.digest()
signature = SIGNER.sign(msg, self.obj)
return bytearray(signature)

View File

@ -0,0 +1,168 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="""Generate an attestation keypair, using the provided Owner private
key to sign the Owner Certificate.""")
parser.add_argument("--key", help="Which endorsement scheme to use (1 or 2)", type=auto_int)
parser.add_argument("--certificate", help="""Optional certificate to store if finalizing the endorsement (hex
encoded), if no private key is specified""")
parser.add_argument("--privateKey", help="""Optional private key to use to create a test certificate (hex encoded),
if no certificate is specified""")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--issuerKey", help="Issuer key (hex encoded, default is batch 1)")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
return parser
def auto_int(x):
return int(x, 0)
def hexstr(bstr):
if (sys.version_info.major == 3):
return binascii.hexlify(bstr).decode()
if (sys.version_info.major == 2):
return binascii.hexlify(bstr)
return ""
def getDeployedSecretV2(dongle, masterPrivate, targetid, issuerKey):
testMaster = PrivateKey(bytes(masterPrivate))
testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False))
targetid = bytearray(struct.pack('>I', targetid))
# identify
apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid
dongle.exchange(apdu)
# walk the chain
nonce = os.urandom(8)
apdu = bytearray([0xe0, 0x50, 0x00, 0x00]) + bytearray([len(nonce)]) + nonce
auth_info = dongle.exchange(apdu)
batch_signer_serial = auth_info[0:4]
deviceNonce = auth_info[4:12]
# if not found, get another pair
#if cardKey != testMasterPublic:
# raise Exception("Invalid batch public key")
dataToSign = bytes(bytearray([0x01]) + testMasterPublic)
signature = testMaster.ecdsa_sign(bytes(dataToSign))
signature = testMaster.ecdsa_serialize(signature)
certificate = bytearray([len(testMasterPublic)]) + testMasterPublic + bytearray([len(signature)]) + signature
apdu = bytearray([0xE0, 0x51, 0x00, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
# provide the ephemeral certificate
ephemeralPrivate = PrivateKey()
ephemeralPublic = bytearray(ephemeralPrivate.pubkey.serialize(compressed=False))
dataToSign = bytes(bytearray([0x11]) + nonce + deviceNonce + ephemeralPublic)
signature = testMaster.ecdsa_sign(bytes(dataToSign))
signature = testMaster.ecdsa_serialize(signature)
certificate = bytearray([len(ephemeralPublic)]) + ephemeralPublic + bytearray([len(signature)]) + signature
apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
# walk the device certificates to retrieve the public key to use for authentication
index = 0
last_pub_key = PublicKey(binascii.unhexlify(issuerKey), raw=True)
device_pub_key = None
while True:
if index == 0:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000')))
elif index == 1:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000')))
else:
break
if len(certificate) == 0:
break
offset = 1
certificateHeader = certificate[offset : offset + certificate[offset-1]]
offset += certificate[offset-1] + 1
certificatePublicKey = certificate[offset : offset + certificate[offset-1]]
offset += certificate[offset-1] + 1
certificateSignatureArray = certificate[offset : offset + certificate[offset-1]]
certificateSignature = last_pub_key.ecdsa_deserialize(bytes(certificateSignatureArray))
# first cert contains a header field which holds the certificate's public key role
if index == 0:
certificateSignedData = bytearray([0x02]) + certificateHeader + certificatePublicKey
# Could check if the device certificate is signed by the issuer public key
# ephemeral key certificate
else:
certificateSignedData = bytearray([0x12]) + deviceNonce + nonce + certificatePublicKey
if not last_pub_key.ecdsa_verify(bytes(certificateSignedData), certificateSignature):
return None
last_pub_key = PublicKey(bytes(certificatePublicKey), raw=True)
if index == 0:
device_pub_key = last_pub_key
index = index + 1
return device_pub_key
if __name__ == '__main__':
from .comm import getDongle
from .ecWrapper import PrivateKey, PublicKey
import hashlib
import struct
import os
import sys
import binascii
args = get_argparser().parse_args()
if args.key == None:
raise Exception("Missing endorsement scheme number")
if args.key != 1 and args.key != 2:
raise Exception("Invalid endorsement scheme number")
if args.targetId == None:
args.targetId = 0x31000002
if args.issuerKey == None:
args.issuerKey = "0490f5c9d15a0134bb019d2afd0bf297149738459706e7ac5be4abc350a1f818057224fce12ec9a65de18ec34d6e8c24db927835ea1692b14c32e9836a75dad609"
if (args.privateKey != None) and (args.certificate != None):
raise Exception("Cannot specify both certificate and privateKey")
privateKey = PrivateKey()
publicKey = str(privateKey.pubkey.serialize(compressed=False))
args.rootPrivateKey = privateKey.serialize()
dongle = getDongle(args.apdu)
publicKey = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId, args.issuerKey)
if args.certificate == None:
apdu = bytearray([0xe0, 0xC0, args.key, 0x00, 0x00])
response = dongle.exchange(apdu)
print("Public key " + hexstr(response[0:65]))
m = hashlib.sha256()
m.update(bytes(b"\xff")) # Endorsement role
m.update(bytes(response[0:65]))
digest = m.digest()
signature = publicKey.ecdsa_deserialize(bytes(response[65:]))
if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True):
raise Exception("Issuer certificate not verified")
if args.privateKey != None:
privateKey = PrivateKey(bytes(args.privateKey.decode('hex')))
dataToSign = bytes(bytearray([0xfe]) + response[0:65])
signature = privateKey.ecdsa_sign(bytes(dataToSign))
args.certificate = hexstr(privateKey.ecdsa_serialize(signature))
if args.certificate != None:
certificate = bytearray.fromhex(args.certificate)
apdu = bytearray([0xe0, 0xC2, 0x00, 0x00, len(certificate)]) + certificate
dongle.exchange(apdu)
print("Endorsement setup finalized")

View File

@ -0,0 +1,178 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="""Generate an attestation keypair, using Ledger to sign the Owner
certificate.""")
parser.add_argument("--url", help="Server URL", default="https://hsmprod.hardwarewallet.com/hsm/process")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--perso", help="""A reference to the personalization key; this is a reference to the specific
Issuer keypair used by Ledger to sign the device's Issuer Certificate""", default="perso_11")
parser.add_argument("--endorsement", help="""A reference to the endorsement key to use; this is a reference to the
specific Owner keypair to be used by Ledger to sign the Owner Certificate""", default="attest_1")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--key", help="Which endorsement scheme to use (1 or 2)", type=auto_int)
return parser
def auto_int(x):
return int(x, 0)
def serverQuery(request, url):
data = request.SerializeToString()
urll = urlparse.urlparse(args.url)
req = urllib2.Request(args.url, data, {"Content-type": "application/octet-stream" })
res = urllib2.urlopen(req)
data = res.read()
response = Response()
response.ParseFromString(data)
if len(response.exception) != 0:
raise Exception(response.exception)
return response
if __name__ == '__main__':
import sys
import os
import struct
if sys.version_info.major == 3:
import urllib.request as urllib2
import urllib.parse as urlparse
else:
import urllib2, urlparse
from .BlueHSMServer_pb2 import Request, Response, Parameter
from .comm import getDongle
args = get_argparser().parse_args()
if args.url == None:
raise Exception("No URL specified")
if args.perso == None:
raise Exception("No personalization specified")
if args.endorsement == None:
raise Exception("No endorsement specified")
if args.key != 1 and args.key != 2:
raise Exception("Invalid endorsement scheme number")
if args.targetId == None:
args.targetId = 0x31000002 # Ledger Blue by default
dongle = getDongle(args.apdu)
# Identify
targetid = bytearray(struct.pack('>I', args.targetId))
apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid
dongle.exchange(apdu)
# Get nonce and ephemeral key
request = Request()
request.reference = "signEndorsement"
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "persoKey"
parameter.name = args.perso
response = serverQuery(request, args.url)
offset = 0
remotePublicKey = response.response[offset : offset + 65]
offset += 65
nonce = response.response[offset : offset + 8]
# Initialize chain
apdu = bytearray([0xe0, 0x50, 0x00, 0x00, 0x08]) + nonce
deviceInit = dongle.exchange(apdu)
deviceNonce = deviceInit[4 : 4 + 8]
# Get remote certificate
request = Request()
request.reference = "signEndorsement"
request.id = response.id
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "persoKey"
parameter.name = args.perso
request.parameters = bytes(deviceNonce)
response = serverQuery(request, args.url)
offset = 0
if sys.version_info.major == 2:
responseLength = ord(response.response[offset + 1])
else:
responseLength = response.response[offset + 1]
remotePublicKeySignatureLength = responseLength + 2
remotePublicKeySignature = response.response[offset : offset + remotePublicKeySignatureLength]
certificate = bytearray([len(remotePublicKey)]) + remotePublicKey + bytearray([len(remotePublicKeySignature)]) + remotePublicKeySignature
apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
# Walk the chain
index = 0
while True:
if index == 0:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000')))
elif index == 1:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000')))
else:
break
if len(certificate) == 0:
break
request = Request()
request.reference = "signEndorsement"
request.id = response.id
request.parameters = bytes(certificate)
serverQuery(request, args.url)
index += 1
# Commit agreement
request = Request()
request.reference = "signEndorsement"
request.id = response.id
response = serverQuery(request, args.url)
# Send endorsement request
apdu = bytearray([0xe0, 0xC0, args.key, 0x00, 0x00])
endorsementData = dongle.exchange(apdu)
request = Request()
request.reference = "signEndorsement"
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "endorsementKey"
parameter.name = args.endorsement
request.parameters = bytes(endorsementData)
request.id = response.id
response = serverQuery(request, args.url)
certificate = bytearray(response.response)
# Commit endorsement certificate
apdu = bytearray([0xe0, 0xC2, 0x00, 0x00, len(certificate)]) + certificate
dongle.exchange(apdu)
print("Endorsement setup finalized")

47
ledgerblue/genCAPair.py Normal file
View File

@ -0,0 +1,47 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Generate a Custom CA public-private keypair and print it to console.")
return parser
def hexstr(bstr):
if (sys.version_info.major == 3):
return binascii.hexlify(bstr).decode()
if (sys.version_info.major == 2):
return binascii.hexlify(bstr)
return ""
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .hexParser import IntelHexParser, IntelHexPrinter
from .hexLoader import HexLoader
from .deployed import getDeployedSecretV1, getDeployedSecretV2
import struct
import binascii
import sys
get_argparser().parse_args()
privateKey = PrivateKey()
publicKey = hexstr(privateKey.pubkey.serialize(compressed=False))
print("Public key : %s" % publicKey)
print("Private key: %s" % privateKey.serialize())

59
ledgerblue/hashApp.py Normal file
View File

@ -0,0 +1,59 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Calculate an application hash from the application's hex file.")
parser.add_argument("--hex", help="The application hex file to be hashed")
return parser
def auto_int(x):
return int(x, 0)
def hexstr(bstr):
if (sys.version_info.major == 3):
return binascii.hexlify(bstr).decode()
if (sys.version_info.major == 2):
return binascii.hexlify(bstr)
return ""
if __name__ == '__main__':
from .hexParser import IntelHexParser
from .hexParser import IntelHexPrinter
import sys
import hashlib
import binascii
args = get_argparser().parse_args()
if args.hex == None:
raise Exception("Missing hex filename to hash")
# parse
parser = IntelHexParser(args.hex)
# prepare data
m = hashlib.sha256()
# consider areas are ordered by ascending address and non-overlaped
for a in parser.getAreas():
m.update(a.data)
dataToSign = m.digest()
print(hexstr(dataToSign))

View File

@ -18,18 +18,129 @@
"""
from Crypto.Cipher import AES
import sys
import struct
import hashlib
import binascii
from .ecWrapper import PrivateKey, PublicKey
from builtins import int
from ecpy.curves import Curve
import os
#from builtins import str
LOAD_SEGMENT_CHUNK_HEADER_LENGTH = 3
MIN_PADDING_LENGTH = 1
SCP_MAC_LENGTH = 0xE
BOLOS_TAG_APPNAME = 0x01
BOLOS_TAG_APPVERSION = 0x02
BOLOS_TAG_ICON = 0x03
BOLOS_TAG_DERIVEPATH = 0x04
BOLOS_TAG_DATASIZE = 0x05
BOLOS_TAG_DEPENDENCY = 0x06
def encodelv(v):
l = len(v)
s = b""
if l < 128:
s += struct.pack(">B", l)
elif l < 256:
s += struct.pack(">B", 0x81)
s += struct.pack(">B", l)
elif l < 65536:
s += struct.pack(">B", 0x82)
s += struct.pack(">H", l)
else:
raise Exception("Unimplemented LV encoding")
s += v
return s
def encodetlv(t, v):
l = len(v)
s = struct.pack(">B", t)
if l < 128:
s += struct.pack(">B", l)
elif l < 256:
s += struct.pack(">B", 0x81)
s += struct.pack(">B", l)
elif l < 65536:
s += struct.pack(">B", 0x82)
s += struct.pack(">H", l)
else:
raise Exception("Unimplemented TLV encoding")
s += v
return s
def str2bool(v):
if v is not None:
return v.lower() in ("yes", "true", "t", "1")
return False
SCP_DEBUG = str2bool(os.getenv("SCP_DEBUG"))
class HexLoader:
def __init__(self, card, cla=0xF0, secure=False, key=None, relative=True):
def scp_derive_key(self, ecdh_secret, keyindex):
retry = 0
# di = sha256(i || retrycounter || ecdh secret)
while True:
sha256 = hashlib.new('sha256')
sha256.update(struct.pack(">IB", keyindex, retry))
sha256.update(ecdh_secret)
# compare di with order
CURVE_SECP256K1 = Curve.get_curve('secp256k1')
if int.from_bytes(sha256.digest(), 'big') < CURVE_SECP256K1.order:
break
#regenerate a new di satisfying order upper bound
retry+=1
# Pi = di*G
privkey = PrivateKey(bytes(sha256.digest()))
pubkey = bytearray(privkey.pubkey.serialize(compressed=False))
# ki = sha256(Pi)
sha256 = hashlib.new('sha256')
sha256.update(pubkey)
#print ("Key " + str (keyindex) + ": " + sha256.hexdigest())
return sha256.digest()
def __init__(self, card, cla=0xF0, secure=False, mutauth_result=None, relative=True, cleardata_block_len=None):
self.card = card
self.cla = cla
self.secure = secure
self.key = key
self.iv = "\x00" * 16
self.createappParams = None
#legacy unsecure SCP (pre nanos-1.4, pre blue-2.1)
self.max_mtu = 0xFE
if not self.card is None:
self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize())
self.scpVersion = 2
self.key = mutauth_result
self.iv = b'\x00' * 16
self.relative = relative
#store the aligned block len to be transported if requested
self.cleardata_block_len=cleardata_block_len
if not (self.cleardata_block_len is None):
if not self.card is None:
self.cleardata_block_len = min(self.cleardata_block_len, self.card.apduMaxDataSize())
# try:
if type(mutauth_result) is dict and 'ecdh_secret' in mutauth_result:
self.scp_enc_key = self.scp_derive_key(mutauth_result['ecdh_secret'], 0)[0:16]
self.scp_enc_iv = b"\x00" * 16
self.scp_mac_key = self.scp_derive_key(mutauth_result['ecdh_secret'], 1)[0:16]
self.scp_mac_iv = b"\x00" * 16
self.scpVersion = 3
self.max_mtu = 0xFE
if not self.card is None:
self.max_mtu = min(self.max_mtu, self.card.apduMaxDataSize()&0xF0)
# except:
# pass
def crc16(self, data):
TABLE_CRC16_CCITT = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
@ -73,41 +184,122 @@ class HexLoader:
return crc
def exchange(self, cla, ins, p1, p2, data):
apdu = bytearray(chr(cla) + chr(ins) + chr(p1) + chr(p2) + chr(len(data))) + bytearray(data)
#wrap
data = self.scpWrap(data)
apdu = bytearray([cla, ins, p1, p2, len(data)]) + bytearray(data)
if self.card == None:
print str(apdu).encode('hex')
print("%s" % binascii.hexlify(apdu))
else:
self.card.exchange(apdu)
# unwrap after exchanged
return self.scpUnwrap(bytes(self.card.exchange(apdu)))
def encryptAES(self, data):
if not self.secure:
def scpWrap(self, data):
if not self.secure or data is None or len(data) == 0:
return data
paddedData = data + '\x80'
while (len(paddedData) % 16) <> 0:
paddedData += '\x00'
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
encryptedData = cipher.encrypt(str(paddedData))
self.iv = encryptedData[len(encryptedData) - 16:]
if self.scpVersion == 3:
if SCP_DEBUG:
print(binascii.hexlify(data))
# ENC
paddedData = data + b'\x80'
while (len(paddedData) % 16) != 0:
paddedData += b'\x00'
if SCP_DEBUG:
print(binascii.hexlify(paddedData))
cipher = AES.new(self.scp_enc_key, AES.MODE_CBC, self.scp_enc_iv)
encryptedData = cipher.encrypt(bytes(paddedData))
self.scp_enc_iv = encryptedData[-16:]
if SCP_DEBUG:
print(binascii.hexlify(encryptedData))
# MAC
cipher = AES.new(self.scp_mac_key, AES.MODE_CBC, self.scp_mac_iv)
macData = cipher.encrypt(encryptedData)
self.scp_mac_iv = macData[-16:]
# only append part of the mac
encryptedData += self.scp_mac_iv[-SCP_MAC_LENGTH:]
if SCP_DEBUG:
print(binascii.hexlify(encryptedData))
else:
paddedData = data + b'\x80'
while (len(paddedData) % 16) != 0:
paddedData += b'\x00'
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
if SCP_DEBUG:
print("wrap_old: "+binascii.hexlify(paddedData))
encryptedData = cipher.encrypt(paddedData)
self.iv = encryptedData[-16:]
#print (">>")
return encryptedData
def scpUnwrap(self, data):
if not self.secure or data is None or len(data) == 0 or len(data) == 2:
return data
if sys.version_info.major == 3:
padding_char = 0x80
else:
padding_char = chr(0x80)
if self.scpVersion == 3:
if SCP_DEBUG:
print(binascii.hexlify(data))
# MAC
cipher = AES.new(self.scp_mac_key, AES.MODE_CBC, self.scp_mac_iv)
macData = cipher.encrypt(data[0:-SCP_MAC_LENGTH])
self.scp_mac_iv = macData[-16:]
if self.scp_mac_iv[-SCP_MAC_LENGTH:] != data[-SCP_MAC_LENGTH:] :
raise BaseException("Invalid SCP MAC")
# consume mac
data = data[0:-SCP_MAC_LENGTH]
if SCP_DEBUG:
print(binascii.hexlify(data))
# ENC
cipher = AES.new(self.scp_enc_key, AES.MODE_CBC, self.scp_enc_iv)
self.scp_enc_iv = data[-16:]
data = cipher.decrypt(data)
l = len(data) - 1
while (data[l] != padding_char):
l-=1
if l == -1:
raise BaseException("Invalid SCP ENC padding")
data = data[0:l]
decryptedData = data
if SCP_DEBUG:
print(binascii.hexlify(data))
else:
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
decryptedData = cipher.decrypt(data)
if SCP_DEBUG:
print("unwrap_old: "+binascii.hexlify(decryptedData))
l = len(decryptedData) - 1
while (decryptedData[l] != padding_char):
l-=1
if l == -1:
raise BaseException("Invalid SCP ENC padding")
decryptedData = decryptedData[0:l]
self.iv = data[-16:]
#print ("<<")
return decryptedData
def selectSegment(self, baseAddress):
data = '\x05' + struct.pack('>I', baseAddress)
data = self.encryptAES(data)
data = b'\x05' + struct.pack('>I', baseAddress)
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def loadSegmentChunk(self, offset, chunk):
data = '\x06' + struct.pack('>H', offset) + chunk
data = self.encryptAES(data)
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
data = b'\x06' + struct.pack('>H', offset) + chunk
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def flushSegment(self):
data = '\x07'
data = self.encryptAES(data)
data = b'\x07'
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def crcSegment(self, offsetSegment, lengthSegment, crcExpected):
data = '\x08' + struct.pack('>H', offsetSegment) + struct.pack('>I', lengthSegment) + struct.pack('>H', crcExpected)
data = self.encryptAES(data)
data = b'\x08' + struct.pack('>H', offsetSegment) + struct.pack('>I', lengthSegment) + struct.pack('>H', crcExpected)
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def validateTargetId(self, targetId):
@ -117,32 +309,132 @@ class HexLoader:
def boot(self, bootadr, signature=None):
# Force jump into Thumb mode
bootadr |= 1
data = '\x09' + struct.pack('>I', bootadr)
data = b'\x09' + struct.pack('>I', bootadr)
if (signature != None):
data += chr(len(signature)) + signature
data = self.encryptAES(data)
data += struct.pack('>B', len(signature)) + signature
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def commit(self, signature=None):
data = b'\x09'
if (signature != None):
data += struct.pack('>B', len(signature)) + signature
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def createAppNoInstallParams(self, appflags, applength, appname, icon=None, path=None, iconOffset=None, iconSize=None, appversion=None):
data = b'\x0B' + struct.pack('>I', applength) + struct.pack('>I', appflags) + struct.pack('>B', len(appname)) + appname
if iconOffset is None:
if not (icon is None):
data += struct.pack('>B', len(icon)) + icon
else:
data += b'\x00'
if not (path is None):
data += struct.pack('>B', len(path)) + path
else:
data += b'\x00'
if not iconOffset is None:
data += struct.pack('>I', iconOffset) + struct.pack('>H', iconSize)
if not appversion is None:
data += struct.pack('>B', len(appversion)) + appversion
# in previous version, appparams are not part of the application hash yet
self.createappParams = None #data[1:]
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def createApp(self, appflags, applength, appname, icon=None, path=None):
data = '\x0B' + struct.pack('>I', applength) + struct.pack('>I', appflags) + chr(len(appname)) + appname
if (icon != None):
data += chr(len(icon)) + icon
if (path != None):
data += chr(len(path)) + path
data = self.encryptAES(data)
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def createApp(self, code_length, data_length=0, install_params_length=0, flags=0, bootOffset=1):
#keep the create app parameters to be included in the load app hash
self.createappParams = struct.pack('>IIIII', code_length, data_length, install_params_length, flags, bootOffset)
data = b'\x0B' + self.createappParams
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def deleteApp(self, appname):
data = '\x0C' + chr(len(appname)) + appname
data = self.encryptAES(data)
data = b'\x0C' + struct.pack('>B',len(appname)) + appname
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def load(self, erase_u8, max_length_per_apdu, hexAreas, bootaddr):
def deleteAppByHash(self, appfullhash):
if len(appfullhash) != 32:
raise BaseException("Invalid hash format, sha256 expected")
data = b'\x15' + appfullhash
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def getVersion(self):
data = b'\x10'
response = self.exchange(self.cla, 0x00, 0x00, 0x00, data)
if sys.version_info.major == 2:
response = bytearray(response)
result = {}
offset = 0
result['targetId'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3]
offset += 4
result['osVersion'] = response[offset + 1 : offset + 1 + response[offset]].decode('utf-8')
offset += 1 + response[offset]
offset += 1
result['flags'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3]
offset += 4
result['mcuVersion'] = response[offset + 1 : offset + 1 + response[offset] - 1].decode('utf-8')
offset += 1 + response[offset]
if (offset < len(response)):
result['mcuHash'] = response[offset : offset + 32]
return result
def listApp(self, restart=True):
if restart:
data = b'\x0E'
else:
data = b'\x0F'
response = self.exchange(self.cla, 0x00, 0x00, 0x00, data)
if sys.version_info.major == 2:
response = bytearray(response)
#print binascii.hexlify(response[0])
result = []
offset = 0
if len(response) > 0:
if response[0] != 0x01:
# support old format
while offset != len(response):
item = {}
offset += 1
item['name'] = response[offset + 1 : offset + 1 + response[offset]].decode('utf-8')
offset += 1 + response[offset]
item['flags'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3]
offset += 4
item['hash'] = response[offset : offset + 32]
offset += 32
result.append(item)
else:
offset += 1
while offset != len(response):
item = {}
#skip the current entry's size
offset += 1
item['flags'] = (response[offset] << 24) | (response[offset + 1] << 16) | (response[offset + 2] << 8) | response[offset + 3]
offset += 4
item['hash_code_data'] = response[offset : offset + 32]
offset += 32
item['hash'] = response[offset : offset + 32]
offset += 32
item['name'] = response[offset + 1 : offset + 1 + response[offset]].decode('utf-8')
offset += 1 + response[offset]
result.append(item)
return result
def load(self, erase_u8, max_length_per_apdu, hexFile, reverse=False, doCRC=True):
if (max_length_per_apdu > self.max_mtu):
max_length_per_apdu = self.max_mtu
initialAddress = 0
if (len(hexAreas) <> 0) and self.relative:
initialAddress = hexAreas[0].getStart()
if self.relative:
initialAddress = hexFile.minAddr()
sha256 = hashlib.new('sha256')
for area in hexAreas:
# stat by hashing the create app params to ensure complete app signature
if self.createappParams:
sha256.update(self.createappParams)
areas = hexFile.getAreas()
if reverse:
areas = reversed(hexFile.getAreas())
for area in areas:
startAddress = area.getStart() - initialAddress
data = area.getData()
self.selectSegment(startAddress)
@ -153,23 +445,50 @@ class HexLoader:
crc = self.crc16(bytearray(data))
offset = 0
length = len(data)
if reverse:
offset = length
while (length > 0):
if length > max_length_per_apdu:
chunkLen = max_length_per_apdu
if length > max_length_per_apdu - LOAD_SEGMENT_CHUNK_HEADER_LENGTH - MIN_PADDING_LENGTH - SCP_MAC_LENGTH:
chunkLen = max_length_per_apdu - LOAD_SEGMENT_CHUNK_HEADER_LENGTH - MIN_PADDING_LENGTH - SCP_MAC_LENGTH
if (chunkLen%16) != 0:
chunkLen -= (chunkLen%16)
else:
chunkLen = length
chunk = data[offset : offset + chunkLen]
sha256.update(chunk)
self.loadSegmentChunk(offset, chunk)
offset += chunkLen
if self.cleardata_block_len and chunkLen%self.cleardata_block_len:
if (chunkLen < self.cleardata_block_len):
raise Exception("Cannot transport not block aligned data with fixed block len")
chunkLen -= chunkLen%self.cleardata_block_len;
# padd with 00's when not complete block and performing NENC
if reverse:
chunk = data[offset-chunkLen : offset]
self.loadSegmentChunk(offset-chunkLen, bytes(chunk))
else:
chunk = data[offset : offset + chunkLen]
sha256.update(chunk)
self.loadSegmentChunk(offset, bytes(chunk))
if reverse:
offset -= chunkLen
else:
offset += chunkLen
length -= chunkLen
self.flushSegment()
self.crcSegment(0, len(data), crc)
if doCRC:
self.crcSegment(0, len(data), crc)
return sha256.hexdigest()
def run(self, hexAreas, bootaddr, signature=None):
initialAddress = 0
if (len(hexAreas) <> 0) and self.relative:
initialAddress = hexAreas[0].getStart()
self.boot(bootaddr - initialAddress, signature)
def run(self, bootoffset=1, signature=None):
self.boot(bootoffset, signature)
def resetCustomCA(self):
data = b'\x13'
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def setupCustomCA(self, name, public):
data = b'\x12' + struct.pack('>B',len(name)) + name + struct.pack('>B',len(public)) + public
self.exchange(self.cla, 0x00, 0x00, 0x00, data)
def runApp(self, name):
data = name
self.exchange(self.cla, 0xD8, 0x00, 0x00, data)

View File

@ -18,139 +18,202 @@
"""
class IntelHexArea:
def __init__(self, start, data):
self.start = start
self.data = data
self.bootAddr = 0
def __init__(self, start, data):
self.start = start
self.data = data
def getStart(self):
return self.start
def getStart(self):
return self.start
def getData(self):
return self.data
def insertAreaSorted(areas, area):
i=0
while i < len(areas):
if area.start < areas[i].start:
break
i+=1
#areas = areas[0:i] + [area]
areas[i:i] = [area]
return areas
def getData(self):
return self.data
class IntelHexParser:
def __init__(self, fileName):
self.areas = []
lineNumber = 0
startZone = None
startFirst = None
current = None
zoneData = ""
file = open(fileName, "r")
for data in file:
lineNumber += 1
data = data.rstrip('\r\n')
if len(data) == 0:
continue
if data[0] <> ':':
raise Exception("Invalid data at line " + str(lineNumber))
data = bytearray(data[1:].decode('hex'))
count = data[0]
address = (data[1] << 8) + data[2]
recordType = data[3]
if recordType == 0x00:
if startZone == None:
raise Exception("Data record but no zone defined at line " + lineNumber)
if startFirst == None:
startFirst = address
current = startFirst
if address <> current:
self.areas.append(IntelHexArea((startZone << 16) + startFirst, zoneData))
zoneData = ""
startFirst = address
current = address
zoneData += data[4:4 + count]
current += count
if recordType == 0x01:
if len(zoneData) <> 0:
self.areas.append(IntelHexArea((startZone << 16) + startFirst, zoneData))
zoneData = ""
startZone = None
startFirst = None
current = None
if recordType == 0x02:
raise Exception("Unsupported record 02")
if recordType == 0x03:
raise Exception("Unsupported record 03")
if recordType == 0x04:
if len(zoneData) <> 0:
self.areas.append(IntelHexArea((startZone << 16) + startFirst, zoneData))
zoneData = ""
startZone = None
startFirst = None
current = None
startZone = (data[4] << 8) + data[5]
if recordType == 0x05:
self.bootAddr = ((data[4]&0xFF) << 24) + ((data[5]&0xFF) << 16) + ((data[6]&0xFF) << 8) + (data[7]&0xFF)
file.close()
# order by start address
def _addArea(self, area):
self.areas = insertAreaSorted(self.areas, area)
def getAreas(self):
return self.areas
def __init__(self, fileName):
self.bootAddr = 0
self.areas = []
lineNumber = 0
startZone = None
startFirst = None
current = None
zoneData = b''
file = open(fileName, "r")
for data in file:
lineNumber += 1
data = data.rstrip('\r\n')
if len(data) == 0:
continue
if data[0] != ':':
raise Exception("Invalid data at line %d" % lineNumber)
data = bytearray.fromhex(data[1:])
count = data[0]
address = (data[1] << 8) + data[2]
recordType = data[3]
if recordType == 0x00:
if startZone == None:
raise Exception("Data record but no zone defined at line " + str(lineNumber))
if startFirst == None:
startFirst = address
current = startFirst
if address != current:
self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData))
zoneData = ""
startFirst = address
current = address
zoneData += data[4:4 + count]
current += count
if recordType == 0x01:
if len(zoneData) != 0:
self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData))
zoneData = ""
startZone = None
startFirst = None
current = None
if recordType == 0x02:
raise Exception("Unsupported record 02")
if recordType == 0x03:
raise Exception("Unsupported record 03")
if recordType == 0x04:
if len(zoneData) != 0:
self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData))
zoneData = ""
startZone = None
startFirst = None
current = None
startZone = (data[4] << 8) + data[5]
if recordType == 0x05:
self.bootAddr = ((data[4]&0xFF) << 24) + ((data[5]&0xFF) << 16) + ((data[6]&0xFF) << 8) + (data[7]&0xFF)
file.close()
def getBootAddr(self):
return self.bootAddr
def getAreas(self):
return self.areas
def getBootAddr(self):
return self.bootAddr
def maxAddr(self):
addr = 0
for a in self.areas:
if (a.start+len(a.data) > addr):
addr = a.start+len(a.data)
return addr
def maxAddr(self):
addr = 0
for a in self.areas:
if (a.start+len(a.data) > addr):
addr = a.start+len(a.data)
return addr
def minAddr(self):
addr = 0xFFFFFFFF
for a in self.areas:
if (a.start < addr):
addr = a.start
return addr
import binascii
class IntelHexPrinter:
def __init__(self, parser=None, eol="\r\n"):
self.areas = []
self.eol = eol
self.bootAddr = 0
# build bound to the parser
if (parser):
self.areas = parser.areas
self.bootAddr = parser.bootAddr
def addArea(self, startaddress, data):
self.areas.append(IntelHexArea(startaddress, data))
def addArea(self, startaddress, data, insertFirst=False):
#self.areas.append(IntelHexArea(startaddress, data))
if (insertFirst):
self.areas = [IntelHexArea(startaddress, data)] + self.areas
else:
#order by start address
self.areas = insertAreaSorted(self.areas, IntelHexArea(startaddress, data))
def setBootAddr(self, bootAddr):
self.bootAddr = int(bootAddr)
def __init__(self, parser=None, eol="\r\n"):
self.areas = []
self.eol = eol
self.bootAddr = 0
# build bound to the parser
if (parser):
for a in parser.areas:
self.addArea(a.start, a.data);
self.bootAddr = parser.bootAddr
def checksum(self, bin):
cks = 0
for b in bin:
cks += b
cks = (-cks) & 0x0FF
return cks
def getAreas(self):
return self.areas
def _emit_binary(self, file, bin):
cks = self.checksum(bin)
file.write((":" + binascii.hexlify(bin) + hex(0x100+cks)[3:] + self.eol).upper())
def getBootAddr(self):
return self.bootAddr
def writeTo(self, fileName, blocksize=32):
file = open(fileName, "w")
for area in self.areas:
off = 0
# force the emission of selection record at start
oldoff = area.start + 0x10000
while off < len(area.data):
# emit a offset selection record
if ((off & 0xFFFF0000) != (oldoff & 0xFFFF0000) ):
self._emit_binary(file, bytearray(("02000004" + hex(0x10000+(area.start>>16))[3:7]).decode('hex')))
def maxAddr(self):
addr = 0
for a in self.areas:
if (a.start+len(a.data) > addr):
addr = a.start+len(a.data)
return addr
# emit data record
if (off+blocksize > len(area.data)):
self._emit_binary(file, bytearray((hex(0x100+(len(area.data)-off))[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:len(area.data)])
else:
self._emit_binary(file, bytearray((hex(0x100+blocksize)[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:off+blocksize])
def minAddr(self):
addr = 0xFFFFFFFF
for a in self.areas:
if (a.start < addr):
addr = a.start
return addr
oldoff = off;
off += blocksize
def setBootAddr(self, bootAddr):
self.bootAddr = int(bootAddr)
def checksum(self, bin):
cks = 0
for b in bin:
cks += b
cks = (-cks) & 0x0FF
return cks
def _emit_binary(self, file, bin):
cks = self.checksum(bin)
s = (":" + binascii.hexlify(bin) + hex(0x100+cks)[3:] + self.eol).upper()
if (file != None):
file.write(s)
else:
print(s)
def writeTo(self, fileName, blocksize=32):
file = None
if(fileName != None):
file = open(fileName, "w")
for area in self.areas:
off = 0
# force the emission of selection record at start
oldoff = area.start + 0x10000
while off < len(area.data):
# emit a offset selection record
if ((off & 0xFFFF0000) != (oldoff & 0xFFFF0000) ):
self._emit_binary(file, bytearray(("02000004" + hex(0x10000+(area.start>>16))[3:7]).decode('hex')))
# emit data record
if (off+blocksize > len(area.data)):
self._emit_binary(file, bytearray((hex(0x100+(len(area.data)-off))[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:len(area.data)])
else:
self._emit_binary(file, bytearray((hex(0x100+blocksize)[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:off+blocksize])
oldoff = off;
off += blocksize
bootAddrHex = hex(0x100000000+self.bootAddr)[3:]
file.write(":04000005"+bootAddrHex+hex(0x100+self.checksum( bytearray(("04000005"+bootAddrHex).decode('hex'))))[3:]+self.eol)
bootAddrHex = hex(0x100000000+self.bootAddr)[3:]
s = ":04000005"+bootAddrHex+hex(0x100+self.checksum( bytearray(("04000005"+bootAddrHex).decode('hex'))))[3:]+self.eol
if (file != None):
file.write(s)
else:
print(s)
file.write(":00000001FF"+self.eol)
s = ":00000001FF"+self.eol
file.close()
if (file != None):
file.write(s)
else:
print(s)
if (file != None):
file.close()

96
ledgerblue/hostOnboard.py Normal file
View File

@ -0,0 +1,96 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="""
.. warning::
Using this script undermines the security of the device. Caveat emptor.
""")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--id", help="Identity to initialize", type=auto_int)
parser.add_argument("--pin", help="Set a PINs to backup the seed for future use")
parser.add_argument("--prefix", help="Derivation prefix")
parser.add_argument("--passphrase", help="Derivation passphrase")
parser.add_argument("--words", help="Derivation phrase")
return parser
def auto_int(x):
return int(x, 0)
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .hexParser import IntelHexParser, IntelHexPrinter
from .hexLoader import HexLoader
import struct
import binascii
import sys
import getpass
import unicodedata
args = get_argparser().parse_args()
if (args.id is None) or args.id > 2:
raise Exception("Missing identity number [0-2]")
dongle = getDongle(args.apdu)
def enter_if_none_and_normalize(hint, strg):
if strg is None: # or len(string) == 0: len 0 is accepted, to specify without being bothered by a message
strg = getpass.getpass(hint)
if len(strg) != 0 :
strg = unicodedata.normalize('NFKD', u''+strg)
return strg
if (args.id < 2):
args.pin = enter_if_none_and_normalize("PIN: ", args.pin)
if args.pin is None or len(args.pin) == 0:
raise Exception("Missing PIN for persistent identity")
elif not args.pin is None:
raise Exception("Can't set a PIN for the temporary identity")
args.prefix = enter_if_none_and_normalize("Derivation prefix: ", args.prefix)
args.passphrase = enter_if_none_and_normalize("Derivation passphrase: ", args.passphrase)
args.words = enter_if_none_and_normalize("Derivation phrase: ", args.words)
if args.pin:
apdudata = bytearray([len(args.pin)]) + bytearray(args.pin, 'utf8')
else:
apdudata = bytearray([0])
if args.prefix:
apdudata += bytearray([len(args.prefix)]) + bytearray(args.prefix, 'utf8')
else:
apdudata += bytearray([0])
if args.passphrase:
apdudata += bytearray([len(args.passphrase)]) + bytearray(args.passphrase, 'utf8')
else:
apdudata += bytearray([0])
if args.words:
apdudata += bytearray([len(args.words)]) + bytearray(args.words, 'utf8')
else:
apdudata += bytearray([0])
apdu = bytearray([0xE0, 0xD0, args.id, 0x00, len(apdudata)]) + apdudata
dongle.exchange(apdu, timeout=3000)

View File

@ -39,7 +39,7 @@ def wrapCommandAPDU(channel, command, packetSize, ble=False):
blockSize = len(command)
result += command[offset : offset + blockSize]
offset = offset + blockSize
while offset <> len(command):
while offset != len(command):
if not ble:
result += struct.pack(">H", channel)
result += struct.pack(">BH", 0x05, sequenceIdx)
@ -51,8 +51,8 @@ def wrapCommandAPDU(channel, command, packetSize, ble=False):
result += command[offset : offset + blockSize]
offset = offset + blockSize
if not ble:
while (len(result) % packetSize) <> 0:
result += "\x00"
while (len(result) % packetSize) != 0:
result += b"\x00"
return bytearray(result)
def unwrapResponseAPDU(channel, data, packetSize, ble=False):
@ -65,16 +65,16 @@ def unwrapResponseAPDU(channel, data, packetSize, ble=False):
if ((data is None) or (len(data) < 5 + extraHeaderSize + 5)):
return None
if not ble:
if struct.unpack(">H", str(data[offset : offset + 2]))[0] <> channel:
if struct.unpack(">H", data[offset : offset + 2])[0] != channel:
raise CommException("Invalid channel")
offset += 2
if data[offset] <> 0x05:
if data[offset] != 0x05:
raise CommException("Invalid tag")
offset += 1
if struct.unpack(">H", str(data[offset : offset + 2]))[0] <> sequenceIdx:
if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx:
raise CommException("Invalid sequence")
offset += 2
responseLength = struct.unpack(">H", str(data[offset : offset + 2]))[0]
responseLength = struct.unpack(">H", data[offset : offset + 2])[0]
offset += 2
if len(data) < 5 + extraHeaderSize + responseLength:
return None
@ -84,18 +84,18 @@ def unwrapResponseAPDU(channel, data, packetSize, ble=False):
blockSize = responseLength
result = data[offset : offset + blockSize]
offset += blockSize
while (len(result) <> responseLength):
while (len(result) != responseLength):
sequenceIdx = sequenceIdx + 1
if (offset == len(data)):
return None
if not ble:
if struct.unpack(">H", str(data[offset : offset + 2]))[0] <> channel:
if struct.unpack(">H", data[offset : offset + 2])[0] != channel:
raise CommException("Invalid channel")
offset += 2
if data[offset] <> 0x05:
if data[offset] != 0x05:
raise CommException("Invalid tag")
offset += 1
if struct.unpack(">H", str(data[offset : offset + 2]))[0] <> sequenceIdx:
if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx:
raise CommException("Invalid sequence")
offset += 2
if (responseLength - len(result)) > packetSize - 3 - extraHeaderSize:

61
ledgerblue/listApps.py Normal file
View File

@ -0,0 +1,61 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="List all apps on the device.")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel
(otherwise, a random one will be generated)""")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true')
return parser
def auto_int(x):
return int(x, 0)
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .deployed import getDeployedSecretV1, getDeployedSecretV2
from .hexLoader import HexLoader
import binascii
args = get_argparser().parse_args()
if args.targetId is None:
args.targetId = 0x31000002
if args.rootPrivateKey is None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
dongle = getDongle(args.apdu)
if args.deployLegacy:
secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
else:
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
loader = HexLoader(dongle, 0xe0, True, secret)
apps = loader.listApp()
while len(apps) != 0:
print(apps)
apps = loader.listApp(False)

View File

@ -17,122 +17,247 @@
********************************************************************************
"""
from secp256k1 import PrivateKey
from .comm import getDongle
from .hexParser import IntelHexParser
from .hexLoader import HexLoader
from .deployed import getDeployedSecretV1, getDeployedSecretV2
DEFAULT_ALIGNMENT = 1024
PAGE_ALIGNMENT = 64
import argparse
import struct
def get_argparser():
parser = argparse.ArgumentParser(description="Load an app onto the device from a hex file.")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--fileName", help="The application hex file to be loaded onto the device")
parser.add_argument("--icon", help="The icon content to use (hex encoded)")
parser.add_argument("--curve", help="""A curve on which BIP 32 derivation is locked ("secp256k1", "prime256r1", or
"ed25519"), can be repeated""", action='append')
parser.add_argument("--path", help="""A BIP 32 path to which derivation is locked (format decimal a'/b'/c), can be
repeated""", action='append')
parser.add_argument("--appName", help="The name to give the application after loading it")
parser.add_argument("--signature", help="A signature of the application (hex encoded)")
parser.add_argument("--signApp", help="Sign application with provided rootPrivateKey", action='store_true')
parser.add_argument("--appFlags", help="The application flags", type=auto_int)
parser.add_argument("--bootAddr", help="The application's boot address", type=auto_int)
parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise
a random one will be generated)""")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true')
parser.add_argument("--apilevel", help="Use given API level when interacting with the device", type=auto_int)
parser.add_argument("--delete", help="Delete the app with the same name before loading the provided one", action='store_true')
parser.add_argument("--params", help="Store icon and install parameters in a parameter section before the code", action='store_true')
parser.add_argument("--tlv", help="Use install parameters for all variable length parameters", action='store_true')
parser.add_argument("--dataSize", help="The code section's size in the provided hex file (to separate data from code, if not provided the whole allocated NVRAM section for the application will remain readonly.", type=auto_int)
parser.add_argument("--appVersion", help="The application version (as a string)")
parser.add_argument("--offline", help="Request to only output application load APDUs", action="store_true")
parser.add_argument("--installparamsSize", help="The loaded install parameters section size (when parameters are already included within the .hex file.", type=auto_int)
parser.add_argument("--tlvraw", help="Add a custom install param with the hextag:hexvalue encoding", action='append')
parser.add_argument("--dep", help="Add a dependency over an appname[:appversion]", action='append')
return parser
def auto_int(x):
return int(x, 0)
return int(x, 0)
def parse_bip32_path(path, apilevel):
if len(path) == 0:
return ""
result = ""
elements = path.split('/')
if apilevel >= 5:
result = result + chr(len(elements))
for pathElement in elements:
element = pathElement.split('\'')
if len(element) == 1:
result = result + struct.pack(">I", int(element[0]))
else:
result = result + struct.pack(">I", 0x80000000 | int(element[0]))
return result
import struct
if len(path) == 0:
return b""
result = b""
elements = path.split('/')
if apilevel >= 5:
result = result + struct.pack('>B', len(elements))
for pathElement in elements:
element = pathElement.split('\'')
if len(element) == 1:
result = result + struct.pack(">I", int(element[0]))
else:
result = result + struct.pack(">I", 0x80000000 | int(element[0]))
return result
parser = argparse.ArgumentParser()
parser.add_argument("--targetId", help="Set the chip target ID", type=auto_int)
parser.add_argument("--fileName", help="Set the file name to load")
parser.add_argument("--icon", help="Set the icon content to use (hex encoded)")
parser.add_argument("--curve", help="Curve on which the derivation is locked (secp256k1|prime256r1|ed25519), can be repeated", action='append')
parser.add_argument("--path", help="BIP 32 path to which the derivation is locked (format decimal a'/b'/c), can be repeated", action='append')
parser.add_argument("--appName", help="Set the application name")
parser.add_argument("--signature", help="Optional application's signature (hex encoded)")
parser.add_argument("--appFlags", help="Set the application flags", type=auto_int)
parser.add_argument("--bootAddr", help="Set the boot address", type=auto_int)
parser.add_argument("--rootPrivateKey", help="Set the root private key")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true')
parser.add_argument("--apilevel", help="Use given API level when interacting with the device", type=auto_int)
def string_to_bytes(x):
import sys
if sys.version_info.major == 3:
return bytes(x, 'ascii')
else:
return bytes(x)
args = parser.parse_args()
if args.apilevel == None:
args.apilevel = 5
if args.targetId == None:
args.targetId = 0x31000002
if args.fileName == None:
raise Exception("Missing fileName")
if args.appName == None:
raise Exception("Missing appName")
if args.appFlags == None:
args.appFlags = 0
if args.rootPrivateKey == None:
privateKey = PrivateKey()
publicKey = str(privateKey.pubkey.serialize(compressed=False)).encode('hex')
print "Generated random root public key : " + publicKey
args.rootPrivateKey = privateKey.serialize().encode('ascii')
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .hexParser import IntelHexParser, IntelHexPrinter
from .hexLoader import HexLoader
from .hexLoader import *
from .deployed import getDeployedSecretV1, getDeployedSecretV2
import struct
import binascii
import sys
parser = IntelHexParser(args.fileName)
if args.bootAddr == None:
args.bootAddr = parser.getBootAddr()
args = get_argparser().parse_args()
path = ""
curveMask = 0xff
if args.curve != None:
curveMask = 0x00
for curve in args.curve:
if curve == 'secp256k1':
curveMask |= 0x01
elif curve == 'prime256r1':
curveMask |= 0x02
elif curve == 'ed25519':
curveMask |= 0x04
else:
raise Exception("Unknown curve " + curve)
if args.apilevel == None:
args.apilevel = 5
if args.targetId == None:
args.targetId = 0x31000002
if args.fileName == None:
raise Exception("Missing fileName")
if args.appName == None:
raise Exception("Missing appName")
if args.appFlags == None:
args.appFlags = 0
if args.rootPrivateKey == None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
if args.apilevel >= 5:
path += chr(curveMask)
if args.path != None:
for item in args.path:
if len(item) <> 0:
path += parse_bip32_path(item, args.apilevel)
else:
args.appName = string_to_bytes(args.appName)
parser = IntelHexParser(args.fileName)
if args.bootAddr == None:
args.bootAddr = parser.getBootAddr()
path = b""
curveMask = 0xff
if args.curve != None:
print "Curve not supported using this API level, ignoring"
if args.path != None:
if len(args.path) > 1:
print "Multiple path levels not supported using this API level, ignoring"
curveMask = 0x00
for curve in args.curve:
if curve == 'secp256k1':
curveMask |= 0x01
elif curve == 'prime256r1':
curveMask |= 0x02
elif curve == 'ed25519':
curveMask |= 0x04
else:
raise Exception("Unknown curve " + curve)
if args.apilevel >= 5:
path += struct.pack('>B',curveMask)
if args.path != None:
for item in args.path:
if len(item) != 0:
path += parse_bip32_path(item, args.apilevel)
else:
if args.curve != None:
print("Curve not supported using this API level, ignoring")
if args.path != None:
if len(args.path) > 1:
print("Multiple path levels not supported using this API level, ignoring")
else:
path = parse_bip32_path(args.path[0], args.apilevel)
if not args.icon is None:
args.icon = bytearray.fromhex(args.icon)
signature = None
if not args.signature is None:
signature = bytearray.fromhex(args.signature)
#prepend app's data with the icon content (could also add other various install parameters)
printer = IntelHexPrinter(parser)
# Use of Nested Encryption Key within the SCP protocol is mandartory for upgrades
cleardata_block_len=None
if args.appFlags & 2:
# Not true for scp < 3
# if signature is None:
# raise BaseException('Upgrades must be signed')
# ensure data can be decoded with code decryption key without troubles.
cleardata_block_len = 16
if not args.offline:
dongle = getDongle(args.apdu)
if args.deployLegacy:
secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
else:
path = parse_bip32_path(args.path[0], args.apilevel)
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
dongle = getDongle(args.apdu)
loader = HexLoader(dongle, 0xe0, not(args.offline), secret, cleardata_block_len=cleardata_block_len)
if args.deployLegacy:
secret = getDeployedSecretV1(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
else:
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
loader = HexLoader(dongle, 0xe0, True, secret)
#tlv mode does not support explicit by name removal, would require a list app before to identify the hash to be removed
if (not (args.appFlags & 2)) and args.delete:
loader.deleteApp(args.appName)
if (not (args.appFlags & 2)):
loader.deleteApp(args.appName)
if (args.tlv):
#if code length is not provided, then consider the whole provided hex file is the code and no data section is split
code_length = printer.maxAddr() - printer.minAddr()
if not args.dataSize is None:
code_length -= args.dataSize
else:
args.dataSize = 0
appLength = 0
for area in parser.getAreas():
appLength += len(area.getData())
installparams = b""
icon = None
if args.icon != None:
icon = bytearray.fromhex(args.icon)
# express dependency
if (args.dep):
for dep in args.dep:
appname = dep
appversion = None
# split if version is specified
if (dep.find(":") != -1):
(appname,appversion) = dep.split(":")
depvalue = encodelv(string_to_bytes(appname))
if(appversion):
depvalue += encodelv(string_to_bytes(appversion))
installparams += encodetlv(BOLOS_TAG_DEPENDENCY, depvalue)
signature = None
if args.signature != None:
signature = bytearray.fromhex(args.signature)
#add raw install parameters as requested
if (args.tlvraw):
for tlvraw in args.tlvraw:
(hextag,hexvalue) = tlvraw.split(":")
installparams += encodetlv(int(hextag, 16), binascii.unhexlify(hexvalue))
loader.createApp(args.appFlags, appLength, args.appName, icon, path)
hash = loader.load(0x0, 0xE0, parser.getAreas(), args.bootAddr)
print "Application hash : " + hash
loader.run(parser.getAreas(), args.bootAddr, signature)
if (not (args.appFlags & 2)) and ( args.installparamsSize is None or args.installparamsSize == 0 ):
#build install parameters
#mandatory app name
installparams += encodetlv(BOLOS_TAG_APPNAME, args.appName)
if not args.appVersion is None:
installparams += encodetlv(BOLOS_TAG_APPVERSION, string_to_bytes(args.appVersion))
if not args.icon is None:
installparams += encodetlv(BOLOS_TAG_ICON, bytes(args.icon))
if len(path) > 0:
installparams += encodetlv(BOLOS_TAG_DERIVEPATH, path)
# append install parameters to the loaded file
param_start = printer.maxAddr()+(PAGE_ALIGNMENT-(args.dataSize%PAGE_ALIGNMENT))%PAGE_ALIGNMENT
# only append install param section when not an upgrade as it has already been computed in the encrypted and signed chunk
printer.addArea(param_start, installparams)
paramsSize = len(installparams)
else:
paramsSize = args.installparamsSize
# split code and install params in the code
code_length -= args.installparamsSize
# create app
#ensure the boot address is an offset
if args.bootAddr > printer.minAddr():
args.bootAddr -= printer.minAddr()
loader.createApp(code_length, args.dataSize, paramsSize, args.appFlags, args.bootAddr|1)
elif (args.params):
paramsSectionContent = []
if not args.icon is None:
paramsSectionContent = args.icon
#take care of aligning the parameters sections to avoid possible invalid dereference of aligned words in the program nvram.
#also use the default MPU alignment
param_start = printer.minAddr()-len(paramsSectionContent)-(DEFAULT_ALIGNMENT-(len(paramsSectionContent)%DEFAULT_ALIGNMENT))
printer.addArea(param_start, paramsSectionContent)
# account for added regions (install parameters, icon ...)
appLength = printer.maxAddr() - printer.minAddr()
loader.createAppNoInstallParams(args.appFlags, appLength, args.appName, None, path, 0, len(paramsSectionContent), args.appVersion)
else:
# account for added regions (install parameters, icon ...)
appLength = printer.maxAddr() - printer.minAddr()
loader.createAppNoInstallParams(args.appFlags, appLength, args.appName, args.icon, path, None, None, args.appVersion)
hash = loader.load(0x0, 0xF0, printer)
print("Application full hash : " + hash)
if (signature == None and args.signApp):
masterPrivate = PrivateKey(bytes(bytearray.fromhex(args.rootPrivateKey)))
signature = masterPrivate.ecdsa_serialize(masterPrivate.ecdsa_sign(bytes(binascii.unhexlify(hash)), raw=True))
print("Application signature: " + binascii.hexlify(signature))
if (args.tlv):
loader.commit(signature)
else:
loader.run(args.bootAddr-printer.minAddr(), signature)

View File

@ -16,36 +16,45 @@
* limitations under the License.
********************************************************************************
"""
from .hexParser import IntelHexParser
from .hexLoader import HexLoader
from .comm import getDongle
import argparse
def auto_int(x):
return int(x, 0)
parser = argparse.ArgumentParser()
parser.add_argument("--targetId", help="Set the chip target ID", type=auto_int)
parser.add_argument("--fileName", help="Set the file name to load")
parser.add_argument("--bootAddr", help="Set the boot address", type=auto_int)
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
def get_argparser():
parser = argparse.ArgumentParser(description="""Load the firmware onto the MCU. The MCU must already be in
bootloader mode.""")
parser.add_argument("--targetId", help="The device's target ID", type=auto_int)
parser.add_argument("--fileName", help="The name of the firmware file to load")
parser.add_argument("--bootAddr", help="The firmware's boot address", type=auto_int)
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--reverse", help="Load HEX file in reverse from the highest address to the lowest", action='store_true')
parser.add_argument("--nocrc", help="Load HEX file without checking CRC of loaded sections", action='store_true')
return parser
args = parser.parse_args()
if __name__ == '__main__':
from .hexParser import IntelHexParser
from .hexLoader import HexLoader
from .comm import getDongle
if args.targetId == None:
raise Exception("Missing targetId")
if args.fileName == None:
raise Exception("Missing fileName")
args = get_argparser().parse_args()
parser = IntelHexParser(args.fileName)
if args.bootAddr == None:
args.bootAddr = parser.getBootAddr()
if args.targetId == None:
raise Exception("Missing targetId")
if args.fileName == None:
raise Exception("Missing fileName")
dongle = getDongle(args.apdu)
parser = IntelHexParser(args.fileName)
if args.bootAddr == None:
args.bootAddr = parser.getBootAddr()
#relative load
loader = HexLoader(dongle, 0xe0, False, None, False)
dongle = getDongle(args.apdu)
loader.validateTargetId(args.targetId)
hash = loader.load(0xFF, 0xF0, parser.getAreas(), args.bootAddr)
loader.run(parser.getAreas(), args.bootAddr)
#relative load
loader = HexLoader(dongle, 0xe0, False, None, False)
loader.validateTargetId(args.targetId)
hash = loader.load(0xFF, 0xF0, parser, reverse=args.reverse, doCRC=(not args.nocrc))
loader.run(args.bootAddr)

View File

@ -0,0 +1,55 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Request the MCU to execute its bootloader.")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise
a random one will be generated)""")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
return parser
def auto_int(x):
return int(x, 0)
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .deployed import getDeployedSecretV1, getDeployedSecretV2
from .hexLoader import HexLoader
import binascii
import sys
args = get_argparser().parse_args()
if args.targetId == None:
args.targetId = 0x31000002
if args.rootPrivateKey == None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
dongle = getDongle(args.apdu)
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
loader = HexLoader(dongle, 0xe0, True, secret)
loader.exchange(0xE0, 0, 0, 0, loader.encryptAES(b'\xB0'));

View File

@ -0,0 +1,60 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="""Remove all Custom CA public keys previously enrolled onto the
device.""")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise
a random one will be generated)""")
return parser
def auto_int(x):
return int(x, 0)
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .hexParser import IntelHexParser, IntelHexPrinter
from .hexLoader import HexLoader
from .deployed import getDeployedSecretV1, getDeployedSecretV2
import struct
import binascii
import sys
args = get_argparser().parse_args()
if args.targetId is None:
args.targetId = 0x31000002
if args.rootPrivateKey is None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
dongle = getDongle(args.apdu)
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
loader = HexLoader(dongle, 0xe0, True, secret)
loader.resetCustomCA()

60
ledgerblue/runApp.py Normal file
View File

@ -0,0 +1,60 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser("Run an application on the device.")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise
a random one will be generated)""")
parser.add_argument("--appName", help="The name of the application to run")
return parser
def auto_int(x):
return int(x, 0)
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .hexParser import IntelHexParser, IntelHexPrinter
from .hexLoader import HexLoader
from .deployed import getDeployedSecretV1, getDeployedSecretV2
import struct
import binascii
import sys
args = get_argparser().parse_args()
if args.targetId is None:
args.targetId = 0x31000002
if args.rootPrivateKey is None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
if args.appName is None:
raise Exception("Missing appname to run")
dongle = getDongle(args.apdu)
loader = HexLoader(dongle, 0xe0)
loader.runApp(args.appName)

View File

@ -17,65 +17,85 @@
********************************************************************************
"""
from .comm import getDongle
from .deployed import getDeployedSecretV2
from secp256k1 import PrivateKey
from Crypto.Cipher import AES
import argparse
import sys
import fileinput
def get_argparser():
parser = argparse.ArgumentParser(description="""Read a sequence of command APDUs from a file and send them to the
device. The file must be formatted as hex, with one CAPDU per line.""")
parser.add_argument("--fileName", help="The name of the APDU script to load")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--scp", help="Open a Secure Channel to exchange APDU", action='store_true')
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Nano S)", type=auto_int)
parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise
a random one will be generated)""")
return parser
def hexstr(bstr):
if (sys.version_info.major == 3):
return binascii.hexlify(bstr).decode()
if (sys.version_info.major == 2):
return binascii.hexlify(bstr)
return ""
def auto_int(x):
return int(x, 0)
return int(x, 0)
parser = argparse.ArgumentParser()
parser.add_argument("--fileName", help="Set the file name to load")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--scp", help="open secure channel to exchange apdu", action='store_true')
parser.add_argument("--targetId", help="Set the chip target ID", type=auto_int)
parser.add_argument("--rootPrivateKey", help="Set the root private key")
if __name__ == '__main__':
from .comm import getDongle
from .deployed import getDeployedSecretV2
from .ecWrapper import PrivateKey
from Crypto.Cipher import AES
import sys
import fileinput
import binascii
args = parser.parse_args()
args = get_argparser().parse_args()
if args.targetId == None:
args.targetId = 0x31100002
if not args.fileName:
#raise Exception("Missing fileName")
file = sys.stdin
else:
file = open(args.fileName, "r")
class SCP:
def __init__(self, dongle, targetId, rootPrivateKey):
self.key = getDeployedSecretV2(dongle, rootPrivateKey, targetId)
self.iv = "\x00" * 16;
def encryptAES(self, data):
paddedData = data + '\x80'
while (len(paddedData) % 16) <> 0:
paddedData += '\x00'
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
encryptedData = cipher.encrypt(str(paddedData))
self.iv = encryptedData[len(encryptedData) - 16:]
return encryptedData
dongle = getDongle(args.apdu)
if args.scp:
if args.rootPrivateKey == None:
privateKey = PrivateKey()
publicKey = str(privateKey.pubkey.serialize(compressed=False)).encode('hex')
print "Generated random root public key : " + publicKey
args.rootPrivateKey = privateKey.serialize().encode('ascii')
scp = SCP(dongle, args.targetId, bytearray.fromhex(args.rootPrivateKey))
for data in file:
data = data.rstrip('\r\n').decode('hex')
if args.scp:
data = bytearray(data)
if data[4] > 0 and len(data)>5:
dongle.exchange(data[0:4] + scp.encryptAES(data[5:5+data[4]]) )
else:
dongle.exchange(data[0:5])
if args.targetId is None:
args.targetId = 0x31100002
if not args.fileName:
#raise Exception("Missing fileName")
file = sys.stdin
else:
dongle.exchange(bytearray(data))
file = open(args.fileName, "r")
class SCP:
def __init__(self, dongle, targetId, rootPrivateKey):
secret = getDeployedSecretV2(dongle, rootPrivateKey, targetId)
self.loader = HexLoader(dongle, 0xe0, True, secret)
def encryptAES(self, data):
return self.loader.scpWrap(data);
def decryptAES(self, data):
return self.loader.scpUnwrap(data);
dongle = getDongle(args.apdu)
if args.scp:
if args.rootPrivateKey is None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
scp = SCP(dongle, args.targetId, bytearray.fromhex(args.rootPrivateKey))
for data in file:
data = data.rstrip('\r\n').decode('hex')
if len(data) < 5:
continue
if args.scp:
data = bytearray(data)
if data[4] > 0 and len(data) > 5:
apduData = data[5: 5 + data[4]]
apduData = scp.encryptAES(bytes(apduData))
result = dongle.exchange(
data[0:4] + bytearray([len(apduData)]) + bytearray(apduData))
else:
result = dongle.exchange(data[0:5])
result = scp.decryptAES(str(result))
if args.apdu:
print("<= Clear " + result.encode('hex'))
else:
dongle.exchange(bytearray(data))

View File

@ -0,0 +1,68 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Enroll a Custom CA public key onto the device.")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--rootPrivateKey", help="""The Signer private key used to establish a Secure Channel (otherwise
a random one will be generated)""")
parser.add_argument("--public", help="The Custom CA public key to be enrolled (hex encoded)")
parser.add_argument("--name", help="""The name to assign to the Custom CA (this will be displayed on screen upon
auth requests)""")
return parser
def auto_int(x):
return int(x, 0)
if __name__ == '__main__':
from .ecWrapper import PrivateKey
from .comm import getDongle
from .hexParser import IntelHexParser, IntelHexPrinter
from .hexLoader import HexLoader
from .deployed import getDeployedSecretV1, getDeployedSecretV2
import struct
import binascii
import sys
args = get_argparser().parse_args()
if args.targetId is None:
args.targetId = 0x31000002
if args.rootPrivateKey is None:
privateKey = PrivateKey()
publicKey = binascii.hexlify(privateKey.pubkey.serialize(compressed=False))
print("Generated random root public key : %s" % publicKey)
args.rootPrivateKey = privateKey.serialize()
if args.public is None:
raise Exception("Missing public key")
if args.name is None:
raise Exception("Missing certificate name")
public = bytearray.fromhex(args.public)
dongle = getDongle(args.apdu)
secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId)
loader = HexLoader(dongle, 0xe0, True, secret)
loader.setupCustomCA(args.name, public)

View File

@ -17,44 +17,56 @@
********************************************************************************
"""
from .hexParser import IntelHexParser
from .hexParser import IntelHexPrinter
from secp256k1 import PrivateKey
import hashlib
import binascii
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Sign an application using the provided Custom CA private key.")
parser.add_argument("--hex", help="The hex file of the application that is to be signed")
parser.add_argument("--key", help="The private key with which to sign the app (hex encoded)")
return parser
def auto_int(x):
return int(x, 0)
return int(x, 0)
parser = argparse.ArgumentParser()
parser.add_argument("--hex", help="Hex file to be signed")
parser.add_argument("--key", help="The private key to sign with (hex encoded)")
def hexstr(bstr):
if (sys.version_info.major == 3):
return binascii.hexlify(bstr).decode()
if (sys.version_info.major == 2):
return binascii.hexlify(bstr)
return ""
args = parser.parse_args()
if __name__ == '__main__':
from .hexParser import IntelHexParser
from .hexParser import IntelHexPrinter
from .ecWrapper import PrivateKey
import hashlib
import sys
import binascii
if args.hex == None:
raise Exception("Missing hex filename to sign")
if args.key == None:
raise Exception("Missing private key")
args = get_argparser().parse_args()
# parse
parser = IntelHexParser(args.hex)
if args.hex == None:
raise Exception("Missing hex filename to sign")
if args.key == None:
raise Exception("Missing private key")
# prepare data
m = hashlib.sha256()
# consider areas are ordered by ascending address and non-overlaped
for a in parser.getAreas():
m.update(a.data)
dataToSign = m.digest()
# parse
parser = IntelHexParser(args.hex)
MASTER_PRIVATE = bytearray.fromhex(args.key)
testMaster = PrivateKey(bytes(MASTER_PRIVATE))
testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False))
# prepare data
m = hashlib.sha256()
# consider areas are ordered by ascending address and non-overlaped
for a in parser.getAreas():
m.update(a.data)
dataToSign = m.digest()
signature = testMaster.ecdsa_sign(bytes(dataToSign), raw=True)
MASTER_PRIVATE = bytearray.fromhex(args.key)
testMaster = PrivateKey(bytes(MASTER_PRIVATE))
#testMasterPublic = bytearray(testMaster.pubkey.serialize(compressed=False))
# test signature before printing it
if testMaster.pubkey.ecdsa_verify(dataToSign, signature, raw=True):
#print "Signer's public: " + binascii.hexlify(testMasterPublic)
print testMaster.ecdsa_serialize(signature).encode('hex')
signature = testMaster.ecdsa_sign(bytes(dataToSign), raw=True)
# test signature before printing it
if testMaster.pubkey.ecdsa_verify(dataToSign, signature, raw=True):
#print("Signer's public: " + binascii.hexlify(testMasterPublic))
print(hexstr(testMaster.ecdsa_serialize(signature)))

View File

@ -0,0 +1,191 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser("Update the firmware by using Ledger to open a Secure Channel.")
parser.add_argument("--url", help="Server URL", default="https://hsmprod.hardwarewallet.com/hsm/process")
parser.add_argument("--apdu", help="Display APDU log", action='store_true')
parser.add_argument("--perso", help="""A reference to the personalization key; this is a reference to the specific
Issuer keypair used by Ledger to sign the device's Issuer Certificate""", default="perso_11")
parser.add_argument("--firmware", help="A reference to the firmware to load")
parser.add_argument("--targetId", help="The device's target ID (default is Ledger Blue)", type=auto_int)
parser.add_argument("--firmwareKey", help="A reference to the firmware key to use")
return parser
def auto_int(x):
return int(x, 0)
def serverQuery(request, url):
data = request.SerializeToString()
urll = urlparse.urlparse(args.url)
req = urllib2.Request(args.url, data, {"Content-type": "application/octet-stream" })
res = urllib2.urlopen(req)
data = res.read()
response = Response()
response.ParseFromString(data)
if len(response.exception) != 0:
raise Exception(response.exception)
return response
if __name__ == '__main__':
import sys
import os
import struct
if sys.version_info.major == 3:
import urllib.request as urllib2
import urllib.parse as urlparse
else:
import urllib2, urlparse
from .BlueHSMServer_pb2 import Request, Response, Parameter
from .comm import getDongle
import sys
args = get_argparser().parse_args()
if args.url == None:
raise Exception("No URL specified")
if args.perso == None:
raise Exception("No personalization specified")
if args.firmware == None:
raise Exception("No firmware specified")
if args.firmwareKey == None:
raise Exception("No firmware key specified")
if args.targetId == None:
args.targetId = 0x31000002 # Ledger Blue by default
dongle = getDongle(args.apdu)
# Identify
targetid = bytearray(struct.pack('>I', args.targetId))
apdu = bytearray([0xe0, 0x04, 0x00, 0x00]) + bytearray([len(targetid)]) + targetid
dongle.exchange(apdu)
# Get nonce and ephemeral key
request = Request()
request.reference = "distributeFirmware11"
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "persoKey"
parameter.name = args.perso
if args.targetId&0xF == 0x3:
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "scpv2"
parameter.name = "dummy"
request.largeStack = True
response = serverQuery(request, args.url)
offset = 0
remotePublicKey = response.response[offset : offset + 65]
offset += 65
nonce = response.response[offset : offset + 8]
# Initialize chain
apdu = bytearray([0xe0, 0x50, 0x00, 0x00, 0x08]) + nonce
deviceInit = dongle.exchange(apdu)
deviceNonce = deviceInit[4 : 4 + 8]
# Get remote certificate
request = Request()
request.reference = "distributeFirmware11"
request.id = response.id
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "persoKey"
parameter.name = args.perso
if args.targetId&0xF == 0x3:
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "scpv2"
parameter.name = "dummy"
request.parameters = bytes(deviceNonce)
request.largeStack = True
response = serverQuery(request, args.url)
offset = 0
if sys.version_info.major == 2:
responseLength = ord(response.response[offset + 1])
else:
responseLength = response.response[offset + 1]
remotePublicKeySignatureLength = responseLength + 2
remotePublicKeySignature = response.response[offset : offset + remotePublicKeySignatureLength]
certificate = bytearray([len(remotePublicKey)]) + remotePublicKey + bytearray([len(remotePublicKeySignature)]) + remotePublicKeySignature
apdu = bytearray([0xE0, 0x51, 0x80, 0x00]) + bytearray([len(certificate)]) + certificate
dongle.exchange(apdu)
# Walk the chain
index = 0
while True:
if index == 0:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052000000')))
elif index == 1:
certificate = bytearray(dongle.exchange(bytearray.fromhex('E052800000')))
else:
break
if len(certificate) == 0:
break
request = Request()
request.reference = "distributeFirmware11"
request.id = response.id
request.parameters = bytes(certificate)
request.largeStack = True
serverQuery(request, args.url)
index += 1
# Commit agreement and send firmware
request = Request()
request.reference = "distributeFirmware11"
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "firmware"
parameter.name = args.firmware
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "firmwareKey"
parameter.name = args.firmwareKey
if args.targetId&0xF == 0x3:
parameter = request.remote_parameters.add()
parameter.local = False
parameter.alias = "scpv2"
parameter.name = "dummy"
request.id = response.id
request.largeStack = True
response = serverQuery(request, args.url)
responseData = bytearray(response.response)
dongle.exchange(bytearray.fromhex('E053000000'))
offset = 0
while offset < len(responseData):
apdu = responseData[offset : offset + 5 + responseData[offset + 4]]
dongle.exchange(apdu)
offset += 5 + responseData[offset + 4]

View File

@ -17,43 +17,48 @@
********************************************************************************
"""
from .hexParser import IntelHexParser
from .hexParser import IntelHexPrinter
from secp256k1 import PublicKey
import hashlib
import binascii
import argparse
def get_argparser():
parser = argparse.ArgumentParser("""Verify that the provided signature is a valid signature of the provided
application.""")
parser.add_argument("--hex", help="The hex file of the signed application")
parser.add_argument("--key", help="The Custom CA public key with which to verify the signature (hex encoded)")
parser.add_argument("--signature", help="The signature to be verified (hex encoded)")
return parser
def auto_int(x):
return int(x, 0)
return int(x, 0)
parser = argparse.ArgumentParser()
parser.add_argument("--hex", help="Hex file to be verify")
parser.add_argument("--key", help="The public key to verify with (hex encoded)")
parser.add_argument("--signature", help="The signature to verify with (hex encoded)")
if __name__ == '__main__':
from .hexParser import IntelHexParser
from .hexParser import IntelHexPrinter
from .ecWrapper import PublicKey
import hashlib
import binascii
args = parser.parse_args()
args = get_argparser().parse_args()
if args.hex == None:
raise Exception("Missing hex filename to verify")
if args.key == None:
raise Exception("Missing public key")
if args.signature == None:
raise Exception("Missing signature")
if args.hex == None:
raise Exception("Missing hex filename to verify")
if args.key == None:
raise Exception("Missing public key")
if args.signature == None:
raise Exception("Missing signature")
# parse
parser = IntelHexParser(args.hex)
# parse
parser = IntelHexParser(args.hex)
# prepare data
m = hashlib.sha256()
# consider areas are ordered by ascending address and non-overlaped
for a in parser.getAreas():
m.update(a.data)
dataToSign = m.digest()
# prepare data
m = hashlib.sha256()
# consider areas are ordered by ascending address and non-overlaped
for a in parser.getAreas():
m.update(a.data)
dataToSign = m.digest()
publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True)
signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature)))
if not publicKey.ecdsa_verify(bytes(dataToSign), signature, raw=True):
raise Exception("Signature not verified")
publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True)
signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature)))
if not publicKey.ecdsa_verify(bytes(dataToSign), signature, raw=True):
raise Exception("Signature not verified")
print "Signature verified"
print("Signature verified")

View File

@ -0,0 +1,57 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Verify a message signature created with Endorsement Scheme #1.")
parser.add_argument("--key", help="The endorsement public key with which to verify the signature (hex encoded)")
parser.add_argument("--codehash", help="The hash of the app associated with the endorsement request (hex encoded)")
parser.add_argument("--message", help="The message associated to the endorsement request (hex encoded)")
parser.add_argument("--signature", help="The signature to be verified (hex encoded)")
return parser
if __name__ == '__main__':
from .ecWrapper import PublicKey
import hashlib
import binascii
args = get_argparser().parse_args()
if args.key == None:
raise Exception("Missing public key")
if args.codehash == None:
raise Exception("Missing code hash")
if args.message == None:
raise Exception("Missing message")
if args.signature == None:
raise Exception("Missing signature")
# prepare data
m = hashlib.sha256()
m.update(bytes(bytearray.fromhex(args.message)))
m.update(bytes(bytearray.fromhex(args.codehash)))
digest = m.digest()
publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True)
signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature)))
if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True):
raise Exception("Endorsement not verified")
print("Endorsement verified")

View File

@ -0,0 +1,59 @@
"""
*******************************************************************************
* Ledger Blue
* (c) 2016 Ledger
*
* 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.
********************************************************************************
"""
import argparse
def get_argparser():
parser = argparse.ArgumentParser(description="Verify a message signature created with Endorsement Scheme #2.")
parser.add_argument("--key", help="The endorsement public key with which to verify the signature (hex encoded)")
parser.add_argument("--codehash", help="The hash of the app associated with the endorsement request (hex encoded)")
parser.add_argument("--message", help="The message associated to the endorsement request (hex encoded)")
parser.add_argument("--signature", help="The signature to be verified (hex encoded)")
return parser
if __name__ == '__main__':
from .ecWrapper import PublicKey
import hashlib
import hmac
import binascii
args = get_argparser().parse_args()
if args.key == None:
raise Exception("Missing public key")
if args.codehash == None:
raise Exception("Missing code hash")
if args.message == None:
raise Exception("Missing message")
if args.signature == None:
raise Exception("Missing signature")
# prepare data
tweak = hmac.new(bytes(bytearray.fromhex(args.codehash)), bytes(bytearray.fromhex(args.key)), hashlib.sha256).digest()
m = hashlib.sha256()
m.update(bytes(bytearray.fromhex(args.message)))
digest = m.digest()
publicKey = PublicKey(bytes(bytearray.fromhex(args.key)), raw=True)
publicKey.tweak_add(bytes(tweak))
signature = publicKey.ecdsa_deserialize(bytes(bytearray.fromhex(args.signature)))
if not publicKey.ecdsa_verify(bytes(digest), signature, raw=True):
raise Exception("Endorsement not verified")
print("Endorsement verified")

View File

@ -5,19 +5,17 @@ from setuptools import setup, find_packages
from os.path import dirname, join
import os
os.environ['SECP_BUNDLED_EXPERIMENTAL'] = "1"
here = dirname(__file__)
setup(
name='ledgerblue',
version='0.1.6',
version='0.1.17',
author='Ledger',
author_email='hello@ledger.fr',
description='Python library to communicate with Ledger Blue/Nano S',
long_description=open(join(here, 'README.md')).read(),
url='https://github.com/LedgerHQ/blue-loader-python',
packages=find_packages(),
install_requires=['hidapi>=0.7.99', 'secp256k1>=0.12.1', 'pycrypto>=2.6.1'],
install_requires=['hidapi>=0.7.99', 'protobuf>=2.6.1', 'pycrypto>=2.6.1', 'future', 'ecpy>=0.8.1', 'pillow>=3.4.0', 'python-u2flib-host>=3.0.2'],
extras_require = {
'smartcard': [ 'python-pyscard>=1.6.12-4build1' ]
},