Skip to content

U2F implementation#56

Open
MatthewWilkes wants to merge 42 commits into
mainfrom
feature/u2f-mode
Open

U2F implementation#56
MatthewWilkes wants to merge 42 commits into
mainfrom
feature/u2f-mode

Conversation

@MatthewWilkes

@MatthewWilkes MatthewWilkes commented May 29, 2022

Copy link
Copy Markdown
Member

Add support for using the TiDAL as a secure 2nd factor authenticator.

This introduces changes to the USB stack to add the appropriate descriptors to identify the TiDAL as a FIDO authenticator, and an implementation of the CTAPHID protocol. The implementation extensively uses memory that is mapped into the Micropython space to enable moving the implementation of user-facing behaviour out of the critical path of the USB stack and into user space. I am not certain if this is required, however an earlier implementation that didn't do this did not work consistently between batches of the ecc108a chip.

It additionally adds provisioning of the ecc108a such that all 16 slots are set up for FIDO keys. This allows up to 16 resident keys to be stored.

Note: If you want to test this easily, I've put the firmware and the web flasher into flasher.tar.gz - download that, go to the directory, and run python -m http.server

Important notes

  1. Only one key can be registered for each domain. This is not an inherent limitation, just an assumption in the implementation as most domains will assume only one key
  2. Only resident keys are supported, but the version of the FIDO/CTAP protocol in use doesn't allow us to signal that, so sites that require resident keys will not work
  3. There appear to still be some race conditions, which may result in having to re-try on login on occasion
  4. The TiDAL cannot operate as a keyboard and a FIDO authenticator at once. There is a setting to switch into MFA mode, which requires a reboot. This is due to the HID descriptors not being correctly recognised when both sets are present. This shouldn't be a limitation, but seems to be in practice.
  5. The ecc108a needs to be provisioned before it can be used. This is irreversable, so do not run this on any chips that you want to use for custom purposes.
  6. The application to manage keys is not particularly user-friendly. The ability to rename keys should be added, as should deprovisioning. Currently, it shows the start of the domain's hash (we could have a lookup?) and the date of registration (which is usually wrong as the system clock is usually wrong)
  7. While the chip counts uses of the keys, we do not fill this data into the CTAP response, we zero it out. Therefore, implementations that require consistent increases of this code will fail (I haven't found any yet)
  8. The attestation certificate is self-signed on a per-slot basis. This is due to not wanting to waste a slot with a manufacturer attestation key, or to have to provision the keys remotely.
  9. The upgrade to IDF 4.4.1 has slightly changed the environment variables needed for building. This is documented in the README.
  10. I have also backported the ota fixes from the tildagon, which involved backporting a newer requests, its compatibility shims, some new shims, and an implementation of redirects from a later micropython-lib/tildagon
  11. This also adds the web flasher - I have attached here a static export of the web flasher to help people test this, as few people still have the build environment set up for this badge

Historical info

The protocol for USB 2 factor auth support is complete and working on some devices. Users can register a TiDAL as an authenticator and authenticate requests, however this isn't working reliably across devices.

This branch contains a custom firmware that allows testing this functionality, however it requires that the crypto chip has been provisioned before it can be used. Once it's provisioned it cannot be reset, it is a destructive operation. We are not confident that the provisioning is correct. In particular, it may well require changes to support attestation. It will also prevent you being able to provision a known key onto the device, which will limit debugging.

This is done by calling
ecc108a.provision_slot() followed by ecc108a.lock_config_zone()

This is likely the problem, the following config (from ecc108a_tools.run()) works on slot 6 only:

Serial number: 01 23 07 9f f1 58 b9 fd ee
Revision number: 0x5100000
Reserved 1: 0xc0
Interface mode: I2C
Reserved 2: 0x90 0x0
I2C address: 0xc0
Reserved 3: 0x0
OTP mode: 0x55 (consumption)
Selector mode: unlimited
TTL reference mode: fixed
Watchdog timeout: 1.3s
Reserved 4: 0x0
UserExtra: 0x0
Selector: 0x0
LockValue: 0x55 (unlocked)
LockConfig: 0x0 (locked)
Reserved 5: 0xff
Reserved 6: 0xff
X509format[0].PublicPosition: 15
X509format[0].TemplateLength: 15
X509format[1].PublicPosition: 15
X509format[1].TemplateLength: 15
X509format[2].PublicPosition: 15
X509format[2].TemplateLength: 15
X509format[3].PublicPosition: 15
X509format[3].TemplateLength: 15
Key 0:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
  UseFlag: 0xff
  UpdateCount: 0
Key 1:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
  UseFlag: 0xff
  UpdateCount: 0
Key 2:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
  UseFlag: 0xff
  UpdateCount: 0
Key 3:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 0
Key 4:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 1300
  UseFlag: 0xff
  UpdateCount: 0
Key 5:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 0013
  UseFlag: 0xff
  UpdateCount: 0
Key 6:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 34
Key 7:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
  UseFlag: 0xff
  UpdateCount: 0
Key 8:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
Key 9:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
Key 10:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 3300
Key 11:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6081
  KeyConfig: 3300
Key 12:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8160
  KeyConfig: 3300
Key 13:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
Key 14:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
Key 15:
  SlotLocked: 1 (unlocked)
  SlotConfig: 8360
  KeyConfig: 3300
  Key use: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff

but this one, where all keys replicate slot 6's config above, no slots work:

Serial number: 01 23 56 14 30 14 c1 f5 ee
Revision number: 0x1100080
Reserved 1: 0xc0
Interface mode: I2C
Reserved 2: 0xa6 0x0
I2C address: 0xc0
Reserved 3: 0x0
OTP mode: 0x55 (consumption)
Selector mode: unlimited
TTL reference mode: fixed
Watchdog timeout: 1.3s
Reserved 4: 0x0
UserExtra: 0x0
Selector: 0x9
LockValue: 0x55 (unlocked)
LockConfig: 0x0 (locked)
Reserved 5: 0xff
Reserved 6: 0xff
X509format[0].PublicPosition: 15
X509format[0].TemplateLength: 15
X509format[1].PublicPosition: 15
X509format[1].TemplateLength: 15
X509format[2].PublicPosition: 15
X509format[2].TemplateLength: 15
X509format[3].PublicPosition: 15
X509format[3].TemplateLength: 15
Key 0:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 136
Key 1:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 6
Key 2:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 1
Key 3:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 0
Key 4:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 0
Key 5:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 0
Key 6:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 1
Key 7:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  UseFlag: 0xff
  UpdateCount: 0
Key 8:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
Key 9:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
Key 10:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
Key 11:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
Key 12:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
Key 13:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
Key 14:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
Key 15:
  SlotLocked: 1 (unlocked)
  SlotConfig: 6083
  KeyConfig: 0033
  Key use: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff

Excessive detail

HID mode

The badge

FIDO USB HID / CTAPHID

Interactions with U2F devices are tunneled over the USB HID protocol. An explanation of how those messages are sent is in the USB HID section below.

These messages are detailed at https://fidoalliance.org/specs/fido-u2f-v1.0-ps-20141009/fido-u2f-hid-protocol-ps-20141009.html

The implementation in the badge is at https://github.com/emfcamp/TiDAL-Firmware/blob/feature/u2f-mode/drivers/tidal_usb/tidal_usb_u2f.c

There are three commands implemented, init, wink and msg. Wink is what makes a device flash for attention. Init begins a session, and 'msg' contains a message for the next protocol down in the stack.

FIDO / CTAP

That next protocol down is FIDO / CTAP. This is detailed in https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html and implemented in the handle_u2f_msg function of https://github.com/emfcamp/TiDAL-Firmware/blob/feature/u2f-mode/drivers/tidal_usb/tidal_usb_u2f.c

This message contains either a register or an authenticate command.

Register

In register mode, we need to allocate a 'handle' for the site. The handle is what the server sends to the device during authentication to identify itself. Many other devices use this to return an encrypted key, so the device then decrypts the handle, and uses that to sign the challenge. We don't have support for that, our handles are integers that reference which of the (few) key slots the crypto chip has is responsible for this site.

This is currently hard-coded to 6, in https://github.com/emfcamp/TiDAL-Firmware/blob/feature/u2f-mode/drivers/tidal_usb/u2f_crypto.c#L122-L124

We return from register with the handle, the pubkey to validate against, an attestation certificate and a signature. Attestation allows consumers to verify that an authenticator is itself authentic. For example, Yubikeys will be attested by Yubikey, to allow validation that the user is using one of an allowlist of known good authenticators

Authenticate

The authenticate message is relative simple, it contains a challenge, a counter and a signature. These are returned and checked by the host machine

Work remaining:

  • Hook up a GUI app that allows users to accept register requests and pick a slot to store the key in
    • The first available slot is used
  • Implement signing with an attestation certificate

USB HID

In order to get this to work, we need to support more USB HID types than just mouse and keyboard. We have therefore switched from the TinyUSB implementation in esp-iot-solution to a vendored-in one with more options. We have moved some of the callbacks into the TiDAL USB driver and added support for the U2F HID types at https://github.com/emfcamp/TiDAL-Firmware/blob/feature/u2f-mode/components/tinyusb/additions/src/descriptors_control.c#L21-L58

The HID implementation at https://github.com/emfcamp/TiDAL-Firmware/blob/feature/u2f-mode/drivers/tidal_usb/tidal_usb_hid.c always includes the standard HID code, but only includes the U2F hooks when enabled.

Work remaining:

  • Exposing multiple HID descriptors was causing issues. You need to choose between CONFIG_TINYUSB_HIDKEYBOARD_ENABLED and CONFIG_TINYUSB_U2FHID_ENABLED in sdkconfig.board. In future, having this switch at run-time rather than build-time would be preferable. This has not been a priority.
    • You must select the U2F mode in the settings panel and reboot the badge. This will stop the USB keyboard functionality from working until it's switched back.

ECC108a chip

The crypto chip is connected over i2c. The datasheet has many details. Some functions have been exposed in micropython, which may assist you in debugging. These are in the ecc108a module. There is also an ec108a_tools module which contains helper code for understanding the config zone.

None of the cryptography functions will work until the config zone is locked, and none of the provisioning and config controls will work once it is locked.

Below is an example transaction, which may be useful for people attempting to validate the behavior. The verify method is not working..

>>> ecc108a.get_pubkey(6)
(b')Y\x0f\xf3\x8e\xcb\\\xc5\xf3W\xc5\xf4+\x9f\x82v\x9e\x92 \x0fe\xe4\xecMrnnh6\x95\x14J', b'\xd0\xa2\xd41\x91\x9ce\xd5\x84D\xe9\x93\xact\xda/\x86\xf3\x07!s\xdc\xa01\xd5\x14[\nk\x03\x87\xa8')
>>> ecc108a.genkey(6)
(b'\xe4`\xb2\x93\xc9\x9bj\x9f\xfbD\x94\x1di\x9b\x96\xc7\x91F\x1c\xcf\x7f\xe4\xbe\x8d4%P\xe3\xbb\x18;\xa0', b'\x0b\x9e\xe8\xf1\x9cB\x06\xb1\x93\xd2&\x7f\xdb\x03\x17\xa82\x99\x18\x1f\t\x1cz\xe5H\xb2\xb3}fM\xabe')
>>> pubkey = ecc108a.get_pubkey(6)
>>> pubkey
(b'\xe4`\xb2\x93\xc9\x9bj\x9f\xfbD\x94\x1di\x9b\x96\xc7\x91F\x1c\xcf\x7f\xe4\xbe\x8d4%P\xe3\xbb\x18;\xa0', b'\x0b\x9e\xe8\xf1\x9cB\x06\xb1\x93\xd2&\x7f\xdb\x03\x17\xa82\x99\x18\x1f\t\x1cz\xe5H\xb2\xb3}fM\xabe')
>>> msg = hashlib.sha256("hello world").digest()
>>> msg
b"\xb9M'\xb9\x93M>\x08\xa5.R\xd7\xda}\xab\xfa\xc4\x84\xef\xe3zS\x80\xee\x90\x88\xf7\xac\xe2\xef\xcd\xe9"
>>> signature = ecc108a.sign(6, msg)
>>> signature
(b'\x08^\xecuD\xfa\xad\xf67\x06\xdb\xf4\\\xbdZ\x99\xd8$0\xfb\x03R\xc3\xfb\n+\xdaqc\xb1\xae\x1e', b'\\\xa6\xcb\xbbK\x01\x180\x02\xcf\xac-\xc2\x03\xd5\xe9d~\xeb\xbe\xda\x7f\xfe\xcf"`\'i\xf0t\x14e')
>>> ecc108a.verify(msg, pubkeky, signature)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: 244 
# N.B:  244 = ATCA_EXECUTION_ERROR

Work remaining:

  • Fix verify method

Debugging end-to-end

I recommend using Wireshark in USB mode and u2fcli for debugging.

MatthewWilkes and others added 19 commits May 28, 2022 11:03
This implements enough of the CTAP1 protocol to complete transactions, albeit with hard-coded responses that don't pass cryptographic muster.
This changes the u2f implementation around somewhat, so it no longer hard-codes
lengths of some variable length fields. It moves the crypto functions out to their
own file, ahead of implementation.
Fix reading of config zone to get all 128 bytes, split initialisation into a helper function and initial (non-working) version of genkey.
This sets up slot configs, suspected bit packing errors causing this not to work. It also exposes the lock functions in Python. Be very careful.
This allows provisioning the 108A to the point that genkey can be called, and offers helper functions for some basic funtionality. These functions can easily brick your badge, so don't run them unless  you know what you're doing. The crypto outputs haven't been verified yet.
At this stage, the critical cryptographic functions of the U2F implementation are linked in. Unfortunately, it's not yet working, currently because the signature parameters aren't parsing correctly.

I'm somewhat concerned that the fido raw message formats document specifies that cryptographic signatures are over the input bytestring, rather than the SHA-256 of that bytestring. The 108A only allows signatures of 32-byte strings, so I've gone that way. I don't think that's the problem I'm seeing yet though, I think it's a more general problem parsing.

In addition, the code is currently hard-coded to use handle 1 for attestation and handle 6 for authentication. This is because only handle 6 is set up correctly on my main test device. Handle 1 will need the keys from keys/* loaded into it - I'm aware that it's silly to put a key in git, but in this case it's not part of the trust path and we have no way of distributing the key without exposing it unless we do it in person in 2024.

No work yet on setting up the UI.
Comment thread drivers/tidal_usb/u2f_crypto.c Outdated
uint8_t signature[64];

ESP_LOGI(TAG, "Calculating digest");
atcab_hw_sha2_256(signature_input, 69, digest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

where does this 69 length come from? the data to be signed over is (1+32+32+L+65, L=1, 131) bytes long isn't it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right, this is wrong. 69 is the length of the thing to be signed during authenticate, 131 is the length to be signed during register. The register signature is there to do the attestation, which we expect to fail anyway (the attestation key isn't provisioned in slot 1), but this would make it doubly wrong.

This might have done it.

@MatthewWilkes

MatthewWilkes commented Jul 7, 2024

Copy link
Copy Markdown
Member Author

The following is a minimal reproducer of the problem:

import ecc108a, hashlib
slot = 6
key = ecc108a.get_pubkey(slot)
message = "This is a test message"
sig = ecc108a.full_sign(slot, message)
hash = hashlib.sha256(message).digest()
assert ecc108a.verify(hash, sig, key)

This works on working slots/devices and fails on non-working ones. Checking the revision matches the one that I have that works is config = ecc108a.read_config(); hex(int.from_bytes(config[4:8], 'little')) == '0x5100000'

@Jonty

Jonty commented Jan 27, 2025

Copy link
Copy Markdown
Member

We should arrange a fourth birthday party for this PR ❤️

@MatthewWilkes

Copy link
Copy Markdown
Member Author

@Jonty It's not THAT old just yet! Not even three!

@MatthewWilkes

Copy link
Copy Markdown
Member Author

@Jonty Although, I might have to get some "EMF 2026? I'm still working on EMF 2022!" badges.

@Jonty

Jonty commented Jan 29, 2025

Copy link
Copy Markdown
Member

@Jonty It's not THAT old just yet! Not even three!

I am bad at years, clearly.

@MatthewWilkes

Copy link
Copy Markdown
Member Author

@Jonty Can this PR still have a 4th birthday party? It's a month away now.

@Jonty

Jonty commented Apr 22, 2026

Copy link
Copy Markdown
Member

HAPRY BIRTHDAY 🎉🎉🎉🎉

@MatthewWilkes MatthewWilkes marked this pull request as ready for review April 23, 2026 21:53
Comment thread keys/private.pem
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

N.B.: This isn't used. It just matches the others in this directory, which acted as the template for creating the dynamic attestation certificates

void push_report(u2f_hid_msg *msg) {
if (upcoming_report_ring_waiting[write_head]) {
// There's already a report waiting to be picked up. This is a fatal error.
ESP_LOGE(TAG, "U2F outbound report buffer full");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It might be better to discard these rather then erroring and writing them in

for i in usable_slots():
print("Looking for matching slot: ", i, settings.get(f"auth_slot_{i}_name"))

if settings.get(f"auth_slot_{i}_name", "") == name:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Add some sort of get-out in the app if the user wipes their badge and wants their keys back

return
elif operation == 3: # Authenticate request
# Force re-discovery of the slot id - TODO: Remove this
slot_id = None

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can we remove this now?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants