NAME

Crypt::MultiKey::PKey::FIDO2 - Use FIDO2 hmac-secret as a password to encrypt/decrypt the private key

DESCRIPTION

FIDO2 is a protocol for hardware authenticators like YubiKeys, particularly in the cheaper "Security Key" variety. FIDO2 is primarily an authentication protocol, not a protocol for shared secrets or encryption/decryption, but FIDO2 provides an extension called "hmac-secret" which allows an application to derive deterministic secret bytes from the authenticator. This can be used as a password, and plugged into the PKey encrypt_private method, which is what this subclass does. In the simplest sense, this module uses a FIDO2 authenticator like a password written on a USB drive, but where the device can't be copied and the password can't be seen without knowing the credential_id and a physical touch on the device. This system is not as secure as if the authenticator was implementing the private half of the PKey internally, but it's a reasonable implementation for the cheaper "Security Key" devices that only support FIDO2. It's also fairly convenient compared to some of the other protocols.

Applications "enroll" with the authenticator by creating a "credential", and then can use that credential to make requests called "assertions". An application constructs an assertion request and sends it to the authenticator which may then prompt the user for confirmation (like a button press or biometric verification) before producing the response. The response may be a success (containing authenticator data, a signature proving possession of the credential private key, and optionally extension outputs such as hmac-secret), or various failure codes such as if the credential didn't exist on that device or the user didn't approve the request. This implementation needs to derive cryptographically secure bytes to use as a password to decrypt the private half of the PKey. The authenticator generates a per-credential secret ("CredRandom") during enrollment. During an assertion request the application supplies a salt value and the authenticator returns HMAC-SHA256(CredRandom, salt) which is suitable as a password. The CredRandom is unique per credential, so every enrollment essentially generates a new password and maintains that password unless the enrollment is deleted. The CredRandom value never leaves the authenticator, so it cannot be copied.

Workflow

Enroll

The hardware key must be present. FIDO2 devices cannot be identified by any kind of serial number, and the only way to differentiate which one the user wants to use is by prompting the user to touch one. So, ideally have *only* the key you want to use plugged in, so this module only has one to choose from.

Call "encrypt_private". This will request creation of a FIDO2 credential from the hardware key (which requires a button press on the device) and store that in attribute "fido2_credential_id". There are several options to configure for this process; see the documentation of that method.

It will then ask the hardware key to perform hmac-secret using that credential (possibly requiring another button press), passing the result through HKDF to generate the password for the parent class's encrypt_private method.

Note that authenticators typically have a limited capacity for credentials or credential-related state. This may be as small as a few dozen. You may wish to enroll one PKey object with a hardware key and then copy the serialized PEM file of the PKey to each environment where that hardware key will be used with Crypt::MultiKey, rather than enrolling a new PKey object in each environment. Of course, if the decrypted private half of that PKey leaks in one environment then it can be used in all of them, so choose carefully when to copy a PKey and when to enroll a new one.

Check

The method "can_obtain_private" will return true if any FIDO2 key with matching device type (the FIDO2 AAGUID) is present in the system. FIDO2 devices are anonymous, with no type of serial number that could identify an individual device, so if a similar device is plugged in this can return true even if the device with the credential is not present. It is not possible to check for existence of a credential within a device without possibly triggering a user-presence or user-validation test (button press, biometric scan) so a passive check method like can_obtain_private can't attempt that.

Decrypt

Call "obtain_private" to issue an assertion request against each connected authenticator with an AAGUID matching the one seen during the Enroll step. If the authenticator possesses the "fido2_credential_id", after a button press (or biometric scan, etc) it will return the HMAC result and this module will proceed to decrypt the private half of the PKey.

Multiple Keys

If you have multiple ::PKey::FIDO2 objects that are candidates for unlocking a Coffer or Vault, testing them one at a time could potentially require multiple button presses by the holder of the hardware key as each credential is tested. To improve the user experience, all of the fido2_credential can be bundled into a single FIDO2 assertion and then a single hardware key button press can test all of them at once. Doing this requires that all the PKeys are using the same "challenge":

my ($secret, $cred_used)= $fido2_device->assert_hmac_secret(
  credential => [ map $_->fido2_credential, @$pkeys ],
  challenge => $pkeys->[0]->challenge
);
if (defined $cred_used) {
  for (grep $_->fido2_credential == $cred_used, @$pkeys) {
    $_->obtain_private(hmac_secret => $secret);
    ...

If the authenticator contains one of those credentials it will let us know which one, and the result of the hmac-secret on that credential's secret. You can then decrypt the PKey object(s) associated with that credential.

ATTRIBUTES

fido2_credential

The credential created by the FIDO2 enrollment process. The fields must include id (the raw bytes of the credential ID) and pubkey (the OpenSSL SubjectPublicKeyInfo encoding of the credential public key) and optionally cose_alg if the algorithm is not "ES256".

fido2_aaguid

The AAGUID of the authenticator which created the FIDO2 credential. This is only unique per device model, but (so long as your devices aren't all the identical model) it can help narrow the detection of whether the correct key is present.

fido2_aaguid_hex

Accessor for the standard GUID hex notation of attribute fido2_aaguid.

challenge

The sha256 of this string is sent to the authenticator as the value to perform HMAC on. The default is "Crypt::MultiKey::PKey::FIDO2". You can change this to something other than the default, but beware that it can limit your ability to efficiently match a ::PKey::FIDO2 object to the corresponding authenticator. If you want to issue challenges for multiple ::PKey::FIDO2 objects in a single request to the authenticator, they all need to have the same value for challenge.

kdf_salt

Random bytes that get combined with the hmac-secret response from the authenticator before performing decryption of the private half of the PKey.

METHODS

create_credential

$pkey->create_credential;            # use first available device
$pkey->create_credential($device);   # use specified device
$pkey->create_credential($dev_path); # resolve /dev/ path to device

This performs FIDO2 enrollment on the specified device, storing the results into the attributes of this object. This is called automatically by encrypt_private unless the credential was already created.

encrypt_private

$pkey->encrypt_private;          # new credential, or find device with credential
$pkey->encrypt_private($device); # specify device to use

Calls "create_credential" (unless "fido2_credential" was already defined) then requests the password from the authenticator, then encrypts the private half of this PKey. It does not clear the private half of this PKey.

can_obtain_private

Returns true if FIDO2 support is available and at least one FIDO2 device with a matching "fido_aaguid" is connected to the system. Returns false on a temporary error (like no matching fido device currently connected) or undef on a fatal error like no FIDO2 library support.

obtain_private

$pkey->obtain_private;                          # find device, request hmac-secret
$pkey->obtain_private(hmac_secret => $secret);  # supply hmac_secret result

This iterates the FIDO2 devices matching the fido2_aaguid looking for one that can answer the hmac-secret challenge. On success, it uses the secret to calculate the password to decrypt_private. Dies on failure.

You can specify the hmac_secret to avoid talking to a device at all. This is helpful when you are querying the device directly and then want to apply the results to one or more PKey objects.

VERSION

version 0.000_001

AUTHOR

Michael Conrad <mike@nrdvana.net>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Michael Conrad.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.