Adding support for backup.

Now it is possible to backup and restore the internal keys to recover a pico fido. The process is splitted in two parts: a list of 24 words and a file, which stores the security key.

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos 2022-10-28 00:31:50 +02:00
parent a42131876f
commit 43cd8869f9
No known key found for this signature in database
GPG Key ID: C0095B7870A4CCD3
6 changed files with 106 additions and 3 deletions

View File

@ -233,7 +233,27 @@ int cbor_config(const uint8_t *data, size_t len) {
} }
has_keydev_dec = true; has_keydev_dec = true;
} }
goto err; //No return }
goto err; //No return
}
else if (vendorCommandId == CTAP_CONFIG_BACKUP) {
if (vendorAutCt.present == false) { // Save
if (has_keydev_dec == false)
CBOR_ERROR(CTAP2_ERR_PIN_AUTH_INVALID);
CBOR_CHECK(cbor_encoder_create_map(&encoder, &mapEncoder, 1));
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x01));
CBOR_CHECK(cbor_encode_byte_string(&mapEncoder, file_get_data(ef_keydev_enc), file_get_size(ef_keydev_enc)));
}
else { // Load
uint8_t zeros[32];
memset(zeros, 0, sizeof(zeros));
flash_write_data_to_file(ef_keydev_enc, vendorAutCt.data, vendorAutCt.len);
flash_write_data_to_file(ef_keydev, zeros, file_get_size(ef_keydev)); // Overwrite ef with 0
flash_write_data_to_file(ef_keydev, NULL, 0); // Set ef to 0 bytes
low_flash_available();
goto err;
} }
} }
else { else {

View File

@ -80,10 +80,11 @@ int cbor_get_info() {
CBOR_CHECK(cbor_encode_uint(&mapEncoder, PICO_FIDO_VERSION)); // firmwareVersion CBOR_CHECK(cbor_encode_uint(&mapEncoder, PICO_FIDO_VERSION)); // firmwareVersion
CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x15)); CBOR_CHECK(cbor_encode_uint(&mapEncoder, 0x15));
CBOR_CHECK(cbor_encoder_create_array(&mapEncoder, &arrayEncoder, 3)); CBOR_CHECK(cbor_encoder_create_array(&mapEncoder, &arrayEncoder, 4));
CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_AUT)); CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_AUT));
CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_KEY_AGREEMENT)); CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_KEY_AGREEMENT));
CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_UNLOCK)); CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_UNLOCK));
CBOR_CHECK(cbor_encode_uint(&arrayEncoder, CTAP_CONFIG_BACKUP));
CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &arrayEncoder)); CBOR_CHECK(cbor_encoder_close_container(&mapEncoder, &arrayEncoder));
CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder)); CBOR_CHECK(cbor_encoder_close_container(&encoder, &mapEncoder));

View File

@ -119,6 +119,7 @@ typedef struct {
#define CTAP_CONFIG_AUT 0x03e43f56b34285e2 #define CTAP_CONFIG_AUT 0x03e43f56b34285e2
#define CTAP_CONFIG_KEY_AGREEMENT 0x1831a40f04a25ed9 #define CTAP_CONFIG_KEY_AGREEMENT 0x1831a40f04a25ed9
#define CTAP_CONFIG_UNLOCK 0x54365966c9a74770 #define CTAP_CONFIG_UNLOCK 0x54365966c9a74770
#define CTAP_CONFIG_BACKUP 0x6b1ede62beff0d5e
// Command status responses // Command status responses

View File

@ -22,6 +22,8 @@
import sys import sys
import argparse import argparse
import platform import platform
from binascii import hexlify
from words import words
try: try:
from fido2.ctap2.config import Config from fido2.ctap2.config import Config
@ -66,9 +68,11 @@ class VendorConfig(Config):
CONFIG_AUT = 0x03e43f56b34285e2 CONFIG_AUT = 0x03e43f56b34285e2
CONFIG_KEY_AGREEMENT = 0x1831a40f04a25ed9 CONFIG_KEY_AGREEMENT = 0x1831a40f04a25ed9
CONFIG_UNLOCK = 0x54365966c9a74770 CONFIG_UNLOCK = 0x54365966c9a74770
CONFIG_BACKUP = 0x6b1ede62beff0d5e
class RESP(IntEnum): class RESP(IntEnum):
KEY_AGREEMENT = 0x01 KEY_AGREEMENT = 0x01
BACKUP = 0x01
def __init__(self, ctap, pin_uv_protocol=None, pin_uv_token=None): def __init__(self, ctap, pin_uv_protocol=None, pin_uv_token=None):
super().__init__(ctap, pin_uv_protocol, pin_uv_token) super().__init__(ctap, pin_uv_protocol, pin_uv_token)
@ -140,12 +144,63 @@ class VendorConfig(Config):
}, },
) )
def backup_save(self, filename):
ret = self._call(
Config.CMD.VENDOR_PROTOTYPE,
{
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_BACKUP,
},
)
data = ret[VendorConfig.RESP.BACKUP]
d = int.from_bytes(skey.get_secure_key(), 'big')
with open(filename, 'wb') as fp:
fp.write(b'\x01')
fp.write(data)
pk = ec.derive_private_key(d, ec.SECP256R1())
signature = pk.sign(data, ec.ECDSA(hashes.SHA256()))
fp.write(signature)
print('Remember the following words in this order:')
for c in range(24):
coef = (d//(2048**c))%2048
print(f'{(c+1):02d} - {words[coef]}')
def backup_load(self, filename):
d = 0
if (d == 0):
for c in range(24):
word = input(f'Introduce word {(c+1):02d}: ')
while (word not in words):
word = input(f'Word not found. Please, tntroduce the correct word {(c+1):02d}: ')
coef = words.index(word)
d = d+(2048**c)*coef
pk = ec.derive_private_key(d, ec.SECP256R1())
pb = pk.public_key()
with open(filename, 'rb') as fp:
format = fp.read(1)[0]
if (format == 0x1):
data = fp.read(60)
signature = fp.read()
pb.verify(signature, data, ec.ECDSA(hashes.SHA256()))
skey.set_secure_key(pk)
self._call(
Config.CMD.VENDOR_PROTOTYPE,
{
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_BACKUP,
VendorConfig.PARAM.VENDOR_AUT_CT: data,
},
)
def parse_args(): def parse_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparser = parser.add_subparsers(title="commands", dest="command") subparser = parser.add_subparsers(title="commands", dest="command")
parser_secure = subparser.add_parser('secure', help='Manages security of Pico Fido.') parser_secure = subparser.add_parser('secure', help='Manages security of Pico Fido.')
parser_secure.add_argument('subcommand', choices=['enable', 'disable', 'unlock'], help='Enables, disables or unlocks the security.') parser_secure.add_argument('subcommand', choices=['enable', 'disable', 'unlock'], help='Enables, disables or unlocks the security.')
parser_backup = subparser.add_parser('backup', help='Manages the backup of Pico Fido.')
parser_backup.add_argument('subcommand', choices=['save', 'load'], help='Saves or loads a backup.')
parser_backup.add_argument('filename', help='File to save or load the backup.')
args = parser.parse_args() args = parser.parse_args()
return args return args
@ -159,8 +214,17 @@ def secure(dev, args):
elif (args.subcommand == 'disable'): elif (args.subcommand == 'disable'):
vcfg.disable_device_aut() vcfg.disable_device_aut()
def backup(dev, args):
vcfg = VendorConfig(Ctap2(dev))
if (args.subcommand == 'save'):
vcfg.backup_save(args.filename)
elif (args.subcommand == 'load'):
vcfg.backup_load(args.filename)
def main(args): def main(args):
print('Pico Fido Tool v1.0') print('Pico Fido Tool v1.2')
print('Author: Pol Henarejos') print('Author: Pol Henarejos')
print('Report bugs to https://github.com/polhenarejos/pico-fido/issues') print('Report bugs to https://github.com/polhenarejos/pico-fido/issues')
print('') print('')
@ -170,6 +234,8 @@ def main(args):
if (args.command == 'secure'): if (args.command == 'secure'):
secure(dev, args) secure(dev, args)
elif (args.command == 'backup'):
backup(dev, args)
def run(): def run():
args = parse_args() args = parse_args()

View File

@ -11,6 +11,12 @@ except:
print('ERROR: keyring module not found! Install keyring package.\nTry with `pip install keyrings.osx-keychain-keys`') print('ERROR: keyring module not found! Install keyring package.\nTry with `pip install keyrings.osx-keychain-keys`')
sys.exit(-1) sys.exit(-1)
try:
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
except:
print('ERROR: cryptography module not found! Install cryptography package.\nTry with `pip install cryptography`')
sys.exit(-1)
def get_backend(use_secure_enclave=False): def get_backend(use_secure_enclave=False):
backend = OSXKeychainKeysBackend( backend = OSXKeychainKeysBackend(
@ -32,6 +38,14 @@ def generate_secure_key(use_secure_enclave=False):
def get_d(key): def get_d(key):
return key.private_numbers().private_value.to_bytes(32, 'big') return key.private_numbers().private_value.to_bytes(32, 'big')
def set_secure_key(pk):
backend = get_backend(False)
try:
backend.delete_password(DOMAIN, USERNAME)
except:
pass
backend.set_password(DOMAIN, USERNAME, pk.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
def get_secure_key(): def get_secure_key():
key = None key = None
try: try:

1
tools/words.py Normal file

File diff suppressed because one or more lines are too long