Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Script to inject key and/or auth counter directly into binary image #26

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
*/.dep
doc/chopstx.info
.vs/
*.json
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is better to be more specific about which files to ignore. What about test/*.json ?

78 changes: 78 additions & 0 deletions src/inject_key_bin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python

#
# Use this script to inject your own private key and authentication counter
# into U2F binary. Might be useful if you want keys to survive firmware updates.
#
# Example:
#
# Generate EC private key with openssl:
# > openssl ecparam -name prime256v1 -genkey -noout -outform der > key.der
#
# Inject generated key into u2f.bin and set auth counter to 100:
# > python3 inject_key_bin.py --key key.der --ctr 100 --bin build/u2f.bin
#
# key will not be modified if --key parameter is not present
# counter will not be modified if --ctr parameter is not present

from __future__ import print_function
from asn1crypto.keys import ECPrivateKey
import hashlib
import argparse
import sys
import struct
import os

# TODO: detect correct offset from .bin file.
stm32f103_offset = 0xB800 # See stm32f103.ld
#efm32hg_offset = ????

parser = argparse.ArgumentParser()
parser.add_argument("--bin", default="build/u2f.bin",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we combine inject_key and inject_key_bin into one? Or even replace inject_key with the inject_key_bin...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think once inject_key_bin is proven to work, it can replace inject_key.

help='.bin file to inject keys into. Or "stdin"')
parser.add_argument("--key", help="EC private key in DER format")
parser.add_argument("--ctr", default=0, type=lambda x: int(x,0), help="Value of auth counter")
parser.add_argument("--offset", default=stm32f103_offset, type=lambda x: int(x,0), help="Offset within file to patch")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, for stm32f103 offset can be 0xf400 or 0xf800 depending on build flags (CUSTOM_ATTESTATION_CERT), for efm32hg it can be 0xb400 or 0xb800. But it is more reliable to count backwards. It should always be 2048 bytes from the end of .bin file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed new code that uses this calculation by default

args = parser.parse_args()

fname, fext = os.path.splitext(args.bin)
assert fext == ".bin"

print("Target binary file:", args.bin)

# load and parse private key
if not args.key:
print("Key not modified")
else:
key_offset = args.offset
print("Injecting key from %s at 0x%0X", (args.key, key_offset)
if args.key == "stdin":
stdin = sys.stdin.buffer if hasattr(sys.stdin, "buffer") else sys.stdin
der = stdin.read()
else:
with open(args.key, "rb") as f:
der = f.read()
key = ECPrivateKey.load(der)

# convert key into raw bytes and calculate it's sha256
key_bytes = bytearray.fromhex(format(key["private_key"].native, '064x'))
key_hash = hashlib.sha256(key_bytes).digest()
# pad key to 1KiB
key_blob = (key_bytes + key_hash).ljust(1024, b"\x00")
assert len(key_blob) == 1024

with open(args.bin, 'r+b') as f:
f.seek(key_offset)
f.write(key_blob)

if not args.ctr:
print("Counter not modified")
else:
ctr_offset = args.offset + 0x400
print("Injecting counter %d at 0x%0X", (args.ctr, ctr_offset)
# fill authentication counter
ctr_blob = struct.pack("<I", args.ctr) * 256

with open(args.bin, 'r+b') as f:
f.seek(ctr_offset)
f.write(ctr_blob)
29 changes: 29 additions & 0 deletions test/yubico_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Basic test
# Requires dfu-utils to be installed.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libu2f-host instead of dfu-utils?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package "u2f-host" on debian. Pushed this change

# This runs through the steps described in https://github.com/Yubico/libu2f-host documentation
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually I run this test suite for every change: https://github.com/google/u2f-ref-code/tree/master/u2f-tests, it is very complete. But having a simple one is a good thing as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok I didn't know about that. I just wanted a simple 'is the key working' test that didn't involve interacting with a website...

# If run with -k argument, the json files are retainsd.
eliotb marked this conversation as resolved.
Show resolved Hide resolved
set -x

user="tomu-$(date +%s)"
pass=$(uuidgen)

curl "https://demo.yubico.com/wsapi/u2f/enroll?username=$user&password=$pass" > regchallenge.json
cat regchallenge.json
echo "Touch key to confirm registration"
u2f-host -a register -o 'https://demo.yubico.com' < regchallenge.json > regdata.json
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add .json files to .gitignore and clean them up at the end of this script?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now by default they are cleaned up at end, unless you add "-k" as first argument. Rather simplistic, but I think it is ok for a basic test script.

#cat regdata.json
curl https://demo.yubico.com/wsapi/u2f/bind -d "username=$user&password=$pass&data=$(cat regdata.json)"
curl "https://demo.yubico.com/wsapi/u2f/sign?username=$user&password=$pass" > challenge.json
cat challenge.json
echo "Touch key to confirm authentication"
u2f-host -a authenticate -o 'https://demo.yubico.com' < challenge.json > signature.json
#cat signature.json
curl https://demo.yubico.com/wsapi/u2f/verify -d "username=$user&password=$pass&data=$(cat signature.json)" > response.json
cat response.json
echo ""

if [ "$1" != "-k" ]
then
rm *.json
fi