Backing up and restoring Solo security keys

first, a rant on self-defeating security measures

Hardware security tokens are a neat thing that more people, including myself, should use.

Unfortunately, every one I've seen has a major flaw, in that it is impossible to make backups of one's master key. In theory, this provides better security - you can't steal a password that can't be copied. In practice, it does the opposite, by encouraging users to work around this limitation in ways that reduce security overall. (There's a parallel to be drawn here to how overly strict password requirements result in users compromising their own security to compensate for not being able to remember the resulting passwords.)

The usual solution proposed by vendors is to just register two keys with every service, and if the primary keys is lost, revoke it and use the secondary keys. This is a terrible non-solution for both the obvious cost reasons (buying two things costs more than buying one thing) and security reasons. Both keys are required to be physically present to register them with new services. This means I have to choose a compromise: either I keep both tokens nearby, greatly increasing the chance that they will both be lost/destroyed at the same time, or I'm not going to bother using these tokens on anything but the most critical services. This hurts adoption. Stop doing this, vendors.

The only vendor I'm aware of that has made an attempt to solve this is DiceKeys. Their solution involves rolling physical dice (okay, neat, if technically unnecessary), then taking a picture of them with a phone app that then loads the resulting key into the hardware (what? just let me enter the numbers, you did not need a camera for this, and I'm not going to depend on this app still working on whatever the current version of Android is in 10 years).

adding the feature, or not I guess

So I picked up a Solo Hacker, the most trustworthy-looking one that was both in stock at the time (there's apparently a major electronics component shortage going on right now) and had support for firmware modification. I fully expected to have to add this feature myself, but was fairly certain it would be possible, as DiceKeys uses the same hardware with different firmware.

Conveniently, it's already there (source). It's just totally undocumented outside of a comment in this one file. Good enough!

#if !defined(IS_BOOTLOADER) && (defined(SOLO_EXPERIMENTAL))
        case CTAPHID_LOADKEY:
            /**
             * Load external key.  Useful for enabling backups.
             * bytes:                   4                     4                      96
             * payload:  version [maj rev patch RFU]| counter_replacement (BE) | master_key |
             * 
             * Counter should be increased by a large amount, e.g. (0x10000000)
             * to outdo any previously lost/broken keys.
            */
            printf1(TAG_HID,"CTAPHID_LOADKEY\n");
            if (len != 104)
            {
                printf2(TAG_ERR,"Error, invalid length.\n");
                ctaphid_send_error(wb->cid, CTAP1_ERR_INVALID_LENGTH);
                return 1;
            }
            param = ctap_buffer[0] << 16;
            param |= ctap_buffer[1] << 8;
            param |= ctap_buffer[2] << 0;
            if (param != 0){
                ctaphid_send_error(wb->cid, CTAP2_ERR_UNSUPPORTED_OPTION);
                return 1;
            }

            // Ask for THREE button presses
            if (ctap_user_presence_test(8000) > 0)
                if (ctap_user_presence_test(2000) > 0)
                    if (ctap_user_presence_test(2000) > 0)
                    {
                        ctap_load_external_keys(ctap_buffer + 8);
                        param = ctap_buffer[7];
                        param |= ctap_buffer[6] << 8;
                        param |= ctap_buffer[5] << 16;
                        param |= ctap_buffer[4] << 24;
                        ctap_atomic_count(param);

                        wb->bcnt = 0;

                        ctaphid_write(wb, NULL, 0);
                        return 1;
                    }

            printf2(TAG_ERR, "Error, invalid length.\n");
            ctaphid_send_error(wb->cid, CTAP2_ERR_OPERATION_DENIED);
            return 1;
#endif

In short, you send a packet containing a "maj rev patch RFU" (I don't know what this is, but it's expected to always be 0, so that's easy), a replacement counter value, and the new master key, then press the button three times.

The counter value here is a number that increases over time. The idea is that if a service sees it stop increasing, or go backwards, it should suspect a replay attack. By giving new, intentionally cloned tokens much higher values, we reduce the chances of any service complaining about the new number being lower than the old number. (In theory, this also invalidates the old token, but there is some debate over whether this is useful, and so not everyone bothers to check.)

building the firmware

The feature we want is behind an #ifdef SOLO_EXPERIMENTAL, so build the firmware with that turned on, and install it to the device.

Install an embedded ARM toolchain (arm-none-eabi-{gcc,binutils}) and the Solo CLI tool, then

$ git clone https://github.com/solokeys/solo1.git --depth 1 --recurse-submodules --branch 4.1.5
$ cd solo1
$ make -C targets/stm32l432/ EXTRA_DEFINES+=-DSOLO_EXPERIMENTAL firmware
$ solo program bootloader targets/stm32l432/solo.hex

generating a key

You'll need 96 bytes of randomness, from a RNG you trust. You can use the Solo itself for this, if you want:

$ solo key rng raw | head -c96 > /tmp/key

Store this file in a reasonable way. For some people this is just "don't write it to an unencrypted disk", for others it's "generate the key on an airgapped machine, spend the next hour memorizing it, then thermite the entire computer". Here, I just put it in /tmp, which is a ramdisk on my machine and probably most modern Unixy systems, and printed out a paper copy.

loading the key onto the device

Now we can use the Solo Python library (included with the CLI tool) to load the key. If doing this on a replacement key, change counter to a bigger number as described above.

import solo, struct
counter = 0
with open('/tmp/key', 'rb') as keyfile:
	data = struct.pack('>II', 0, counter) + keyfile.read()
device = solo.client.find()
device.send_data_hid(0x62, data)

The LED will turn red. When it does, press the button three times. When it turns green again, you're done.

disclaimers

I have tested this by wiping and reloading the same physical key; I have not tested on separate hardware. I see no reason it shouldn't work.

Obviously, this does not help you in the case where your key is stolen, rather than lost or destroyed. Adjust for your threat model. I'm just telling you stuff you can do.

I am not an Official Security Expert™, I just play one on the internet.