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
73 changes: 73 additions & 0 deletions src/inject_key_bin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/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
#
# 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

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=0xB800, 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.

Maybe we could guess by looking at the .bin file whether it is Tomu binary of stm32f1x? Or at least we could have two preset values one for each of them. Also I don't remember if playing with make flags can change the offset.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All builds share same .ld file, so I think offsets are really constant. Maybe this argument can be removed in favour of a constant. In a pinch, a user can edit the .py to change the value?

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'm wrong about all builds sharing same .ld. There is also efm32hg.ld

args = parser.parse_args()

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

print("Target binary file:", args.bin)
print("Key injection offset 0x%0X" % args.offset)

# load and parse private key
if not args.key:
print("Key not modified")
else:
print("Injecting key from", args.key)
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(args.offset)
f.write(key_blob)

if not args.ctr:
print("Counter not modified")
else:
print("Injecting counter", args.ctr)
# fill authentication counter
ctr_blob = struct.pack("<I", args.ctr) * 256

with open(args.bin, 'r+b') as f:
f.seek(args.offset + 1024)
Copy link
Owner

Choose a reason for hiding this comment

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

if offset is "Offset within file to patch", why do we need to add 1024 here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

args.offset is offset to start of patching when both key and ctr are patched. Yet I want to be able to patch either independently.
I made it a commandline parameter because I wasn't sure that it would be constant across all targets. Actually I think it is effectively a constant

f.write(ctr_blob)
18 changes: 18 additions & 0 deletions test/yubico_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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...

set -x
curl 'https://demo.yubico.com/wsapi/u2f/enroll?username=tomu&password=test' > 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=tomu&password=test&data=$(cat 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.

can we generate random user/password here to avoid possible conflicts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reasonable request.

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've done this

curl 'https://demo.yubico.com/wsapi/u2f/sign?username=tomu&password=test' > 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=tomu&password=test&data=$(cat signature.json)" > response.json
cat response.json
echo ""