Skip to content

Conversation

steelman
Copy link
Contributor

Use FIDO2 devices with hmac-secret extension to generate key encryption key (KEK) instead of passphrase processed with argon2 algorithm.

To use such device a user needs to point to one with --fido2-device option for either repo-create or key change-passphrase. For reading, borg tries to find which of plugged in devices can be used to decrypt KEK (see fido2.Fido2Operations.find_device()).

  1. This is not a final version by any means. It is a working one, however. Please review it and report any improvement you think this patch may use.
  2. I've introduced fido2 module that wraps around Yubico's module. I haven't updated the requirements because I believe this should remain optional. I've made the wrapper to be loadable without Yubico's module and defer reporting errors until an attempt to use it.
  3. I've added help messages for command-line options I've introduced, but nothing more. Please tell, where to add a broader description of how FIDO2 device works with borg.
  4. This code is somewhat inspired by how systemd uses FIDO2 (see libfido2-util.c).
  5. I've used borg for quite some time now, but in rather simplistic setups and I am not very familiar with the whole code base, so I probably missed some usage scenarios.
  6. I haven't really tested the find_device() function with more devices because I have just one.
  7. I haven't added tests and I am not quite sure how to do it when a piece of hardware is involved and by definition not a reproducible one.

@ThomasWaldmann
Copy link
Member

ThomasWaldmann commented Aug 16, 2025

Thanks for the PR!

Yes, guess this would be a good new feature, we even have a related ticket #4549.

I only had a quick glance at the code:

  • maybe move fido2 module into to the borg.crypto package
  • i feel a bit uncomfortable about talking to fido2 hw without the user requesting it (like with find_device()). when talking to hw, things can go wrong (permissions, driver issues, malfunctioning hw, ...) and it would be bad if borg were just hanging or crashing in there even though the user maybe does not even use the fido2 hw with the current repository.
  • we can think about whether a commandline option or an environment variable is better to specify the device that shall be used.
  • yes, related package dependencies must be optional. borg can support installing them, see pyproject.toml -> project.optional-dependencies.
  • ruff seems to be unhappy: https://github.com/borgbackup/borg/actions/runs/16999373707/job/48197734819?pr=8995
  • black also seems to be unhappy: https://github.com/borgbackup/borg/actions/runs/16999373689/job/48197734800?pr=8995 - maybe install the pre-commit hook, see our developer docs.

Enable reading of a new passphrase not only from BORG_NEW_PASSPHRASE
environement variable but also from a passcommandoor a file descriptor.

If Passphrase.new() is called from FlexiKey.change_passphrase(), do
not read a passphrase from regular sources (i.e. BORG_PASSPHRASE
et al.) if it is available via one of the "new" ones (BORG_NEW_PASSPHRASE
et al.). This makes it possible to change passphrases in
fully non-interactive manner.
@steelman
Copy link
Contributor Author

steelman commented Aug 17, 2025

Thanks for the PR!

Yes, guess this would be a good new feature, we even have a related ticket #4549.

I only had a quick glance at the code:

* maybe move `fido2` module into to the `borg.crypto` package

Done.

* i feel a bit uncomfortable about talking to fido2 hw without the user requesting it (like with `find_device()`). when talking to hw, things can go wrong (permissions, driver issues, malfunctioning hw, ...) and it would be bad if borg were just hanging or crashing in there even though the user maybe does not even use the fido2 hw with the current repository.

TL;DR — only device that created credential_id would accept it and return an assertion. Feeding credential_id and salt to a device different that the one used to create credantial_id is harmless.

I believe I can explain a little bit more how FIDO2 hmac-secret and it may ease your mind. Upon the creation of FIDO2 backed KEK three values are created

  • credential_id — derived from values passed to the make_credential() method and device's secret key
  • salt — a random value, as usual
  • secret — the KEK

We store the first two as an "encrypted key". They, together with the particular device, are needed to retrieve the secret in the future. Only the device used to create the credential_id we have, would accept it and return a secret (to get the secret we need the right salt value). Considering that FIDO2 devices are meant to be indistinguishable from one another for privacy reasons (there is a specification for enterprise tokens which are identifiable) the automatic process of matching credential_id from an encrypted key against all available devices during decryption is more convenient for users than asking them (in a rear event when they have more than one device attached) to identify the token manually.

Also — in the current setup (at least mine and this may need more investigation) a device asks for user presence confirmation (up in FIDO2 docs) which means pushing/touching a button on the device for it to return the secret value. Indeed, the requirement of up is disabled in the process of finding the matching device, but this is a pre-flight phase as described in the FIDO2 specifications linked in the comments in find_device().

* we can think about whether a commandline option or an environment variable is better to specify the device that shall be used.

OK, I don't have strong opinions about the UI (personally I like it to be as user controllable as possible).

However, do keep in mind that as I explained above it is crucial to manually specify the device only when calling encrypt_key_file() (and thus encrypt_key_file_fido2()). And even in that case the device would perform user-presence check i.e. wait for the user to touch the device. Therefore, with most users having a single FIDO2 token using the first one returned by CtapHidDevice.list_devices() and waiting for user to confirm it is the right one by touching its button seems quite reasonable (see systemd's fido2_find_device_auto() function)

* yes, related package dependencies must be optional. borg can support installing them, see `pyproject.toml` -> `project.optional-dependencies`.

Done.

* ruff […]

* black […]

Done.

I also extended the changes to passphrase.py and moved them to a separate commit.

EDIT: One thing I haven't touched is key export/backup.

Unlike in case of passphrase based keys where only the encrypted key needs to be backed up and the passphrase is supposed to be remembered, the recovery scenario for hardware backed encryption needs to consider hardware failure. In systemd-cryptenroll(1) there is an option to enroll a recovery key. Technically it is just a very long random passphrase which in case of LUKS is enough because LUKS works like borg repokey that is an encrypted key is stored in volume's header. Unlike in borg (AFAIK), there may be up to 8 key slots in which a volume key may be encrypted with different passphrases (systemd-cryptenroll encodes the output of FIDO2 hmac-secret with base64 and passes it as a passphrase to cryptsetup).

In borg (AFAIK) there is no way to have a master repository key encrypted with several different KEKs except of manual (as of now) fiddling with keyfiles. In this case the easiest way to enable a user to recover from a hardware failure is to show them a secret value as returned by FIDO2 device and later accept it typed in should user's token fail (base64 or otherwise encoded for convenience). This method might also complement current paperkey export of passphrase based KEKs.

Use FIDO2 devices with hmac-secret extension to generate
key encryption key (KEK) instead of passphrase processed
with argon2.
@ThomasWaldmann
Copy link
Member

Quite a lot of tests are failing. This is because fido2 related code gets used even if it was not explicitly requested (and there usually is no fido2 device while testing anyway).

While this could be fixed/adapted, it also proves my point about that things can go wrong and that fido2 functionality should not be used if not explicitly requested. My point is mainly about hw, driver and OS-level malfunctioning (which we can't fix nor foresee).

Just as an unrelated example: I use a yubikey for misc. purposes and sometimes I have to physically disconnect/reconnect it to resolve it being unresponsive in some application (while it worked flawlessly shortly before in another application). We must avoid that such malfunctions block borg from working.

There could be an env var BORG_FIDO2_DEVICE (or so), default value "none". The env variable could be queried at the relevant places and if it is "none", no fido2 code gets executed (behaves as before this PR). Other values could be a device spec or "auto" to automatically find a device.

@steelman
Copy link
Contributor Author

Quite a lot of tests are failing. This is because fido2 related code gets used even if it was not explicitly requested (and there usually is no fido2 device while testing anyway).

I'll am back at work so I will fix it not as soon as I would like to, but I will.

While this could be fixed/adapted, it also proves my point about that things can go wrong and that fido2 functionality should not be used if not explicitly requested. My point is mainly about hw, driver and OS-level malfunctioning (which we can't fix nor foresee).

IMHO requesting to work with a repository encrypted with fido2 backed key qualifies as an explicit request to use fido2 functionality. The only exception from such notion would be a request to manually enter a KEK in case of disaster recovery (broken/lost device).

There could be an env var BORG_FIDO2_DEVICE (or so), default value "none". The env variable could be queried at the relevant places and if it is "none", no fido2 code gets executed (behaves as before this PR). Other values could be a device spec or "auto" to automatically find a device.

That sounds like a good idea. How about supporting also "auto" value triggering find_device()?

Tested with repo-info only. More code paths need to be covered.

To be continued.
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.

2 participants