Taylor ‘Riastradh’ Campbell campbell+fidocrypt@mumble.net
Fidocrypt is a technique by which a server can store a secret in the credential during U2F/FIDO/webauthn registration, and retrieve it again during signin. As long as the server erases its copy of the secret, and as long as the U2F device isn't badly designed (see below on security), the secret cannot be retrieved again except by U2F/FIDO/webauthn signin with the device.
- WARNING: Fidocrypt is new and has had little scrutiny. There may be security issues. Caveat lector.
For example, if a server holds the share of a key to encrypt a user's password vault, you might store this share as the fidocrypt secret -- and store the same share with every credential registered by the user, so any one of their U2F keys not only serves to sign in but also serves to decrypt the share.
The server-side credential storage of fidocrypt is necessarily slightly different from standard webauthn credential storage. Signin with fidocrypt provides the same authentication guarantee as standard webauthn -- it just also lets you retrieve a secret at the same time.
Fidocrypt works with:
- any U2F key
- any FIDO2 key using ECDSA over NIST P-256 (i.e.,
ES256
/P-256
, in terms of RFC 8152) - any FIDO2 key with the hmac-secret extension
This should cover essentially every U2F/FIDO key ever made as of 2020; the vast majority support ECDSA over NIST P-256 anyway.
This C implementation of fidocrypt is based on Yubico's libfido2 library. This implemention is limited to ECDSA over NIST P-256 and Ed25519 -- it does not (yet) support any other credential types, but this covers the vast majority of U2F/FIDO models already.
Credit: I first learned about the technique to derive secrets with U2F from Joseph Birr-Paxton's blog post on abusing U2F to ‘store’ keys.
I tweaked it to store a secret with the credential that can be decrypted with a key derived from the device, rather than just exposing the key directly, so that you can easily store the same secret encrypted differently with many U2F devices. Then I added support for the hmac-secret extension so it is less of an abuse of the protocol.
Other implementations of the same basic idea:
The following make
targets are included:
make
ormake all
-- build library and example program and run testsmake install
-- install library, example program, and man pagesmake check
-- build and run testsmake lib
-- build librarymake install-lib
-- install librarymake install-shlib
-- install just the shared library, no soname link or header files
The makefile respects the variables prefix
, bindir
, includedir
,
libdir
, mandir
, man1dir
, man3dir
, and, for staged installation,
DESTDIR
. The libdir
(default $(prefix)/lib
) must match during
build and install; otherwise the shared library will not be found at
run-time.
The following C preprocessor macros may be set in CPPFLAGS
, with,
e.g., CPPFLAGS=-DHAVE_FIDO_DEV_SET_SIGMASK
, to enable use of newer
features or bug fixes in libfido2:
HAVE_FIDO_ASSERT_SET_HMAC_SECRET
(libfido2 >=1.7.0, Yubico/libfido2#256)HAVE_FIDO_CRED_AUTHDATA_RAW_PTR
(libfido2 >=1.6.0, Yubico/libfido2#212)HAVE_FIDO_DEV_SET_SIGMASK
(libfido2 >=1.7.0, Yubico/libfido2#251, but broken on macOS, Yubico/libfido2#650)HAVE_FIDO_ED25519
(libfido2 >=1.4.0)HAVE_FIDO_ES256_PK_FROM_EC_KEY_FIX
(libfido2 >=1.11.0, Yubico/libfido2#546)HAVE_FIDO_RSA
(libfido2 >=1.4.0)
fidocrypt.c implements a command that stores a short secret in a file encrypted with any one of a set of enrolled security keys in a file.
Example:
$ export FIDOCRYPT_RPID=fidocrypt.example.com
$ fidocrypt enroll -N Falken -u falken -n yubi5nano example.crypt
tap key `yubi5nano' key to enroll; waiting...
tap key `yubi5nano' again to verify; waiting...
$ fidocrypt list example.crypt
1 yubi5nano
$ fidocrypt get example.crypt
fidocrypt: specify an output format (-F)
Usage: fidocrypt get -F <format> <cryptfile>
$ fidocrypt get -F base64 example.crypt
tap key; waiting...
yTpyXp1Hk3F48Wx3Mp7B2gNOChPyPW0VOH3C7l5AM9A=
$ fidocrypt enroll -N Falken -u falken -n redsolokey example.crypt
tap a key that's already enrolled; waiting...
tap key `redsolokey' to enroll; waiting...
tap key `redsolokey' again to verify; waiting...
$ fidocrypt get -F base64 example.crypt
tap key; waiting...
yTpyXp1Hk3F48Wx3Mp7B2gNOChPyPW0VOH3C7l5AM9A=
$ fidocrypt rename -n redsolokey example.crypt blacksolokey
$ fidocrypt list example.crypt
2 blacksolokey
1 yubi5nano
The fidocrypt command is implemented in terms of the following libfidocrypt functions extending the libfido2 API:
-
fido_cred_encrypt(cred, assert, idx, payload, payloadlen, &ciphertext, &ciphertextlen)
Given a credential, such as one obtained with
fido_dev_make_cred
or derived from webauthnnavigator.credential.create
, encrypt the payload and return a ciphertext. Providing nonnullassert
allows use of the hmac-secret extension.You should then store
ciphertext
alongside the credential id ofcred
-- and not the public key ofcred
-- so you can later pass it tofido_assert_decrypt
to verify an authenticator and recover the payload. -
fido_assert_decrypt(assert, idx, ciphertext, ciphertextlen, &payload, &payloadlen)
Given an assertion, such as one obtained with
fido_dev_get_assert
or derived from webauthnnavigator.credential.get
, verify the assertion and decrypt the ciphertext, or fail if the assertion does not match the credential.fido_assert_decrypt
implies the same authentication security asfido_assert_verify
against a known public key. You must only usefido_assert_decrypt
, and notfido_assert_verify
, if you are using fidocrypt, since for fidocrypt's secrecy properties the ‘public key’ must be kept secret.
On most U2F devices, the public key is effectively a pseudorandom function (with nonuniform distribution) of the credential id. Typically, the credential id (or key handle, in the older U2F nomenclature) is either
-
an authenticated ciphertext containing a private key generated on the device, under a symmetric secret key stored on the device, as in current Yubico models; or
-
a random input to a PRF which is used to derive the private key from it, along with an authenticator on the random input under a symmetric key stored on the device, as in past Yubico models, SoloKeys (key generation: (1), (2); key loading: (1), (2)), and likely other devices too.
In principle, a badly designed U2F device could expose the public key in the credential id. I don't know of any that do this, and it would be quite a waste of space -- the credential id already has to determine a ~256-bit private key, and usually has a ~128-bit authenticator on it; on top of that, a public key is usually at least 32 bytes long.
That said, like all U2F-based systems, you should use fidocrypt as one factor in multi-factor authentication -- use it to encrypt a single share of a key to decrypt a password vault or laptop disk, not the whole key, and combine it with another key derived from a password or software storage device or similar.
-
Without the hmac-secret extension, an eavesdropper or MITM on the channel between the authenticator and the server can recover the encryption key that will decrypt the ciphertext on the server. (The adversary would still need the ciphertext on the server, of course.)
Normally this threat model is not relevant -- typically, the channel goes through a browser talking to the server over TLS; the browser is trusted anyway, and TLS should defeat any eavesdropper and MITM on the network. However, it might be relevant for NFC devices that implement only U2F or FIDO2 without the hmac-secret extension.
-
With the hmac-secret extension, fidocrypt defends against such an eavesdropper or MITM.
All arithmetic to compute ECDSA public key recovery is done in OpenSSL. Any side channel attacks on OpenSSL, such as on OpenSSL's generic bignum arithmetic, may carry over to fidocrypt. There are two weak mitigations for timing side channel attacks:
-
The vulnerable arithmetic is computed only once at registration time and once at each signin. Thus, an adversary not in control of the user's device has limited opportunities to repeat measurements to refine a statistical model of the secrets -- it's only when the user chooses to sign in.
-
The key derived by the vulnerable arithmetic is used for only one purpose -- to verify and decrypt a ciphertext stored on the server. So it is useful to an adversary only if they also compromise the ciphertext on the server.
Timing side channel attacks are serious, and you might rightly choose a different implementation of fidocrypt on the basis of them. Nevertheless, you can still use fidocrypt as a factor in a multifactor system, combining the secret with others (say) derived from the user's password, to raise the difficulty for the adversary -- especially if users are already using U2F as a second factor to sign in anyway.