Skip to content

Instantly share code, notes, and snippets.

@joostd
Last active December 16, 2023 15:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joostd/e8e2d4df17d9588ac135b1669a6b35c5 to your computer and use it in GitHub Desktop.
Save joostd/e8e2d4df17d9588ac135b1669a6b35c5 to your computer and use it in GitHub Desktop.
Adam Langley's ctap1.py translated to python3
# 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