Last active
December 16, 2023 15:01
-
-
Save joostd/e8e2d4df17d9588ac135b1669a6b35c5 to your computer and use it in GitHub Desktop.
Adam Langley's ctap1.py translated to python3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Run with a single argument: a /dev/hidrawX path. | |
# If you don't have udev setup to allow access to U2F tokens, you may need to | |
# chown the device to your user before running this script. | |
# If you don't know which hidraw to use, try removing and reinserting your | |
# token. Then the device with the most recent ctime is the one you want. | |
# | |
# Once running, press the token's button twice. The first press will trigger a | |
# registration, the second an authentication. | |
# | |
# Python3 version of https://www.imperialviolet.org/binary/ctap1.py | |
# See https://www.imperialviolet.org/2017/08/13/securitykeys.html | |
import sys | |
import struct | |
import time | |
import hashlib | |
import base64 | |
if len(sys.argv) < 2: | |
print('Please give /dev/hidrawX path as first argument') | |
dev = open(sys.argv[1], 'rb+') | |
def zeroPad(requestedLen, x): | |
return x + (b'\x00' * max(requestedLen-len(x), 0)) | |
chanID = b'' | |
def write(usbCmd, payload): | |
prefix = b'\x00' + chanID | |
i = -1 | |
while len(payload) > 0: | |
if i < 0: | |
packet = prefix + struct.pack('>BH', usbCmd, len(payload)) | |
else: | |
packet = prefix + struct.pack('B', i) | |
todo = min(len(payload), 65 - len(packet)) | |
packet += payload[:todo] | |
payload = payload[todo:] | |
dev.write(zeroPad(65, packet)) | |
dev.flush() | |
print('> ' + packet.hex()) | |
i += 1 | |
def read(): | |
packet = dev.read(64) | |
print('< ' + packet.hex()) | |
packet = packet[4:] | |
(usbCmd, length) = struct.unpack('>BH', packet[:3]) | |
packet = packet[3:] | |
reply = packet[:min(length, len(packet))] | |
while length > len(reply): | |
packet = dev.read(64) | |
packet = packet[5:] | |
reply += packet[:min(length - len(reply), len(packet))] | |
return usbCmd, reply | |
def transact(cmd, p1, p2, body): | |
payload = struct.pack('BBBB', 0, cmd, p1, p2) | |
if len(body) != 0: | |
payload += struct.pack('>BH', 0, len(body)) | |
payload += body | |
if len(body) != 0: | |
payload += b'\x00' * 3 | |
else: | |
payload += b'\x00' * 2 | |
write(0x83, payload) | |
(usbCmd, reply) = read() | |
return struct.unpack('>H', reply[-2:])[0], reply[:-2] | |
# Request a channel from the device. | |
dev.write(zeroPad(65, bytes.fromhex('00ffffffff860008cbf160f6e427b502'))) | |
reply = dev.read(64) | |
idOffset = 4 + 1 + 2 + 8 | |
chanID = reply[idOffset:idOffset+4] | |
while True: | |
challenge_hash = hashlib.sha256(b'challenge').digest() | |
appid_hash = hashlib.sha256(b'https://example.com').digest() | |
status, reply = transact(1, 3, 0, challenge_hash + appid_hash) | |
if status == 0x6985: | |
time.sleep(0.5) | |
continue | |
if status != 0x9000: | |
print(hex(status)) | |
sys.exit(1) | |
print(reply.hex()) | |
print('Public key: ' + reply[1:66].hex()) | |
key_handle_length = reply[66] | |
key_handle = reply[67:67 + key_handle_length] | |
print('Key handle: ' + key_handle.hex()) | |
reply = reply[67 + key_handle_length:] | |
# This is a fragile way to get an certificate length; just to keep | |
# things short. | |
cert_len = struct.unpack('>H', reply[2:4])[0] | |
print('-----BEGIN CERTIFICATE-----') | |
print(base64.b64encode(reply[:4+cert_len])) | |
print('-----END CERTIFICATE-----') | |
print('Signature: ' + reply[4+cert_len:].hex()) | |
break | |
while True: | |
challenge_hash = hashlib.sha256(b'challenge').digest() | |
appid_hash = hashlib.sha256(b'https://example.com').digest() | |
status, reply = transact(2, 3, 0, challenge_hash + appid_hash + | |
bytes([len(key_handle)]) + key_handle) | |
if status == 0x6985: | |
time.sleep(0.5) | |
continue | |
if status != 0x9000: | |
print(hex(status)) | |
sys.exit(1) | |
(flags, counter) = struct.unpack('>BI', reply[:5]) | |
if flags & 1: | |
print('User-presence tested') | |
print('Counter: %d' % counter) | |
print('Signature: ' + reply[5:].hex()) | |
break |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment