SafeKey Protocol (CS)

This chapter contains a description of the protocol used for communication with device’s custom features.

Overview

To access the custom features, host application should send to the FIDO2 device a FIDO U2F or FIDO2 request, formatted in proper way, as specified in this chapter.

Message and Packet Structure

U2F Messages

Messages are send over U2F, using U2F_AUTHENTICATE command (or its equivalent for FIDO2). U2F_AUTHENTICATE command is formatted as below:

where:

  • Off is short from offset

  • Len is short from length

Data for the custom commands are sent within the U2F_KEY_HANDLE field. Data in U2F_CHALLENGE field are unused due to a possible future change in the API, where they would be provided by the browser itself.

Data for the custom commands are received within the U2F_SIGNATURE field.

Send Data To Device

Complete Message

The data to send should be prefixed with the Command ID, e.g. as below:

Outgoing Format

Message

Incoming Format

Message

where RESULT is an operation execution result.

Receive Data From Device

Outgoing Format

Message

Incoming Format

Single Chunk Request

Complete Message

After completing all of the chunks, the full message is as follows:

Custom Commands

Here all implemented custom commands for Custom Storage handling are listed. Both command parameters and results are transported as a CBOR (Concise Binary Object Representation) encoded structure.

CBOR (Concise Binary Object Representation) is a binary data serialization format loosely based on JSON. Like JSON it allows the transmission of data objects that contain name–value pairs, but in a more concise manner. This increases processing and transfer speeds at the cost of human-readability.

where for given command:

  • BACKPAR is {SLOTID, IV, HMAC, DATA};

  • plus + sign means a requirement for operation named by this specific column, whereas minus - sign on the contrary;

  • column Au, short from authentication, marks authentication requirement with the LOGIN command, before using this command;

  • column Bt, short from button, marks touch-button press requirement after the command is called, to proceed further;

  • column Ac, short from activation, marks device’s activation requirement before using the command.

The ID parameter is unique per origin, it could be thought of it, as if internally it would be processed as a logical pair {origin, ID}. commands prefixed with TEST_ are available only in the development build, for unit testing. In the production build the code is not compiled.

STATUS (0x0)

Param: None
Returns: None
Description: Reserved for the future use - to return status of the device. At the moment no operation is executed Requires activation and authorization. 
Error codes: None

TEST_PING (0x1)

Param: Any / Raw
Returns: Any / Raw
Description: Returns sent data. Used to test the transport protocol. The received data have to be equal to the sent. Test command - not available in the production release. Requires activation.

READ (0x2)

Param: {ID}
Returns: {ID, ...}
Description: Reads CBOR record from the Custom Storage. Data are returned as-is from the data slot. ID is per origin. It is not possible to read data cross-origin. Fails, if the ID do not exist origin-wise. Requires activation and authorization.

Error

  • CTAP2_ERR_INVALID_CBOR_TYPE - on invalid CBOR data;

  • CTAP2_ERR_REQUEST_TOO_LARGE - on too long ID;

  • ERR_NOT_FOUND - on non-existing ID;

  • ERR_SUCCESS - on success.

WRITE (0x3)

Param: {ID, ...}
Returns: None
Description: Writes CBOR record to the Custom Storage. Fails, if record with given ID exists origin-wise, or the write to the device has not been 
confirmed. Requires activation and authorization.

Error

  • ERR_BAD_FORMAT - on invalid CBOR data, or on too long ID;

  • ERR_ALREADY_IN_DATABASE - on already existing ID;

  • ERR_FAILED_LOADING_DATA - on read-after-write check fail for just written record;

  • ERR_SUCCESS - on success.

TEST_CLEAR (0x4)

Param: None
Returns: None
Description: Test command. Clears all the custom storage data, by erasing all the occupied pages. Leaves other user data intact. Test command - not available in the production release. Requires activation and authorization.
Error codes: None

FREE (0x5)

Param: None
Returns: {BYTES, SLOTS}
Description: Return free bytes and slots in the whole Custom Storage pace.
Requires activation and authorization.
Error codes: None

REMOVE (0x6)

Param: {ID}
Returns: None
Description: Remove record with the given ID from the storage. ID is searched origin-wise. Fails, if the ID is not found in the given origin space. Requires activation and authorization.

Error

  • ERR_NOT_FOUND - on non-existing ID;

  • CTAP2_ERR_INVALID_CBOR_TYPE - on invalid CBOR data;

  • CTAP2_ERR_REQUEST_TOO_LARGE - on too long ID;

  • ERR_BAD_FORMAT - on invalid CBOR data, or on too long ID;

  • ERR_SUCCESS - on success.

LIST (0x7)

Param: {PAGE}
Returns: CBOR list of ID
Description: Return all IDs written to the Custom Storage for given origin. Due to memory limitations it implements paging with PAGE parameter, which is a requested page from range [0,9]. Returns results from range [PAGE*8, (PAGE+1)*8). For a complete list, it has to be called 10 times. Requires activation and authorization.

Error

  • BAD_FORMAT - on PAGE being outside [0,10) range;

  • CTAP2_ERR_INVALID_CBOR_TYPE - on invalid CBOR data written to the CS;

  • ERR_SUCCESS - on success.

LOGIN (0x8)

Param: {PIN, _TP}
Returns: None
Description: Authorize user with given auth token _TP. Upon success allows  access to the Custom Storage commands, by loading the CS’s master AES encryption key. Token is valid for 60 seconds. Resets current PIN attempts counter to default value (8) on success, decrements it on failure. If the boot or main- attempt counters are equal 0, fails.

Requires activation, authorization, and confirming the call by pressing the touch-button.

Error

  • ERR_NOT_ALLOWED - calling command in current device’s state is not allowed. Such states include being in blocked state (either completely, or only in this power cycle);

  • ERR_USER_NOT_PRESENT - on not confirming the call by the user;

  • ERR_INVALID_PIN - on invalid PIN;

  • CTAP2_ERR_INVALID_CBOR_TYPE - on CBOR parsing issue;

  • ERR_SUCCESS - on success.

LOGOUT (0x9)

Param: None
Returns: None
Description: Clear temporary authorization token, making it not possible
to use the Custom Storage commands until user has logged again. Called automatically on the next CS commands call, if 60 seconds have passed.
Requires activation and authorization.
Error codes: None

PIN_SET (0xA)

Param: {NEW_PIN}
Returns: None
Description: Set new PIN, when it is not yet initialized
Requires activation, authorization, and confirming the call by pressing the touch-button.

Error

  • ERR_NOT_ALLOWED - calling command in current device’s state is not allowed. Such states include being in blocked state (either completely, or only in this power cycle);

  • ERR_BAD_FORMAT - on too small input data;

  • ERR_INVALID_PIN - when the new PIN is not in the accepted range length (4, 63];

  • CTAP2_ERR_INVALID_CBOR_TYPE - on invalid input data;

  • CTAP2_ERR_REQUEST_TOO_LARGE - on too long input data;

  • ERR_SUCCESS - on success.

PIN_CHANGE (0xB)

Param: {PIN, NEW_PIN}
Returns: None
Description: Change PIN to NEW_PIN, provided PIN match current PIN.
Requires activation, authorization, and confirming the call by pressing the touch-button.

Error

  • ERR_NOT_ALLOWED - calling command in current device’s state is not allowed. Such states include being in blocked state (either completely, or only in this power cycle);

  • ERR_BAD_FORMAT - on too small input data;

  • ERR_INVALID_PIN - when the provided PIN is not equal to current one, or when the new PIN is not in the accepted range length (4, 63];

  • CTAP2_ERR_INVALID_CBOR_TYPE - on invalid input data;

  • CTAP2_ERR_REQUEST_TOO_LARGE - on too long input data;

  • ERR_SUCCESS - on success.

PIN_ATTEMPTS (0xC)

Param: None
Returns: {COUNTER}
Description: Return current value of PIN attempts counter. 
Requires activation and authorization.

Error

  • ERR_NOT_ALLOWED - if the PIN is not set;

  • ERR_SUCCESS - on success.

FACTORY_RESET (0xD)

Param: None
Returns: None
Description: Logs out. Clears the CS (by removing its AES encryption key, and further clearing all the pages), and executes FIDO2 reset, which clears PIN and FIDO U2F / FIDO2 secrets. 
Requires activation, authorization, and confirming the call by pressing the touch-button.
Error codes: None

BACKUP_READ (0xE)

Param: {SLOTID}
Returns: {SLOTID, IV, HMAC, DATA}
Description: Makes a backup of the data slot SLOTID, which is a number in range [0,80). Returns IV used for AES-CBC encryption, HMAC for authorization, and encrypted data DATA. Requires calling BACKUP_BEGIN command before running. Uses encryption key from the current backup session
Requires activation and authorization.

Error

  • ERR_FAILED_LOADING_DATA - on error reading the slot data;

  • ERR_BAD_FORMAT - returned when the {SLOTID} is out of range;

  • ERR_NOT_ALLOWED - command not allowed, when the backup session is not active;

  • ERR_SUCCESS - on success.

BACKUP_WRITE (0xF)

Param: {SLOTID, IV, HMAC, DATA}
Returns: None
Description: Writes backed up data record to the device. Requires IV needed for AES-CBC decryption, HMAC for authorization, and encrypted data DATA. Requires calling BACKUP_BEGIN command before running.
Requires activation and authorization.

Error

  • ERR_FAILED_LOADING_DATA - on error writing the backup record data to the device;

  • ERR_BAD_FORMAT - returned when the {SLOTID} is out of range, or the backup record is empty, or DATA/IV size not a multiply of 16;

  • ERR_NOT_ALLOWED - command not allowed, when the backup session is not active;

  • ERR_INVALID_CHECKSUM - if calculated HMAC is not equal to provided one, stop operation;

  • ERR_ALREADY_IN_DATABASE - record cannot be imported, since there is one already with such ID;

  • ERR_SUCCESS - on success.

BACKUP_BEGIN (0x10)

Param: {PASS, SALT}
Returns: {SALT}
Description: Begin backup process. For data export, SALT parameter should be unused. It will be generated on the device, and returned. It has to be stored along with the backups, to allow restoring the AES key. For data import, SALT is necessary to recover the AES key from the PASS. PASS is the backup passphrase, with size in range [16,256], and it should be generated by the device (e.g. with GET_RANDOM command).
Requires activation, authorization, and confirming the call by pressing the touch-button.

Error

  • ERR_USER_NOT_PRESENT - on not confirming the call by the user;

  • ERR_FAILED_LOADING_DATA - on error creating output data - namely SALT;

  • ERR_BAD_FORMAT - returned when the PASS, or SALT, are too short;

  • ERR_SUCCESS - on success.

BACKUP_FINISH (0x11)

Param: None
Returns: None
Description: Finish backup process. Clear the backup session AES encryption key, and clear session  in-progress flag.
Requires activation and authorization.
Error codes: None

ACTIVATION_BEGIN (0x12)

Param: None
Returns: {NONCE, SN}
Description: Begin activation process. Call ACTIVATION_FINISH to finish. After successful activation device allows to execute custom commands, and access to the Custom Storage. Activation process cannot be in-progress already, otherwise error is returned. NONCE is a random number generated by the device, and SN is its serial number. Command could be called any time, without authorization, or activation, but requires touch button confirmation.
Error codes: None

Error

  • ERR_FAILED_LOADING_DATA - on error creating output data - namely {NONCE, SN};

  • ERR_NOT_ALLOWED - fails, if activation is in process already (in that case ACTIVATION_FINISH should be called beforehand to close current activation session);

  • ERR_SUCCESS - on success.

ACTIVATION_FINISH (0x13)

Param: {SIGNATURE, STATE}
Returns: None
Description: Finish activation process. Should be called after ACTIVATION_BEGIN, otherwise will return error. Set the activation state to STATE (either 0 - disabled, or 1 - enabled), given the SIGNATURE is valid. Command could be called any time (given ACTIVATION_BEGIN was called, and the activation session is started), without authorization, or activation.

Error

  • ERR_NOT_ALLOWED - fails, if activation is not in progress already (in that case ACTIVATION_BEGIN should be called beforehand);

  • ERR_BAD_FORMAT - if signature field is of invalid length, than expected (64 bytes);

  • ERR_INVALID_SIGNATURE - if the signature is invalid;

  • ERR_SUCCESS - on success.

GET_RANDOM (0x14)

Param: None
Returns: {RANDOM} Description: Return 32 bytes of data from device’s hardware random generator. Requires activation and authorization.

Error

  • ERR_FAILED_LOADING_DATA - fail, if the output CBOR structure cannot be parsed;

  • ERR_SUCCESS - on success.

TEST_REBOOT (0x15)

Param: None
Returns: None
Description: Test command. Simulate reboot by resetting power-cycle PIN attempt counter.
Test command - not available in the production release. Requires activation and authorization.
Error codes: None

PIN Protection

All Custom Storage commands requiring authorization should be parametrized with a temporary authorization token (field _TP), merged into the request CBOR structure.

The _TP token could be validated only with the LOGIN command, and will be invalidated automatically after 60 seconds, or after LOGOUT command call.

The _TP parametrization should be done automatically in the high level JavaScript API. For instance for low-level API, in case of command PIN_CHANGE - instead of call arguments {PIN, NEW_PIN}, {PIN, NEW_PIN, _TP} should be used, where _TP contains the value of temporary authentication token.

Error Codes

In the implementation all error names are prefixed with ERR_.

JavaScript API

To interact with the device over a browser, a simple high-level API JavaScript is provided.

High-Level API

Wrappers over a low-level API will are provided over each available CS command.

JavaScript high-level API commands list:

CS_STATUS( None ) -> None
CS_READ( ID ) -> ID,
CS_WRITE( ID, … ) -> None
CS_FREE( None ) -> Bytes, Slots
CS_REMOVE( ID ) -> None
CS_LIST( PAGE ) -> [ID1,ID2,..ID8]
CS_LOGIN( PIN, _TP ) -> None
CS_LOGOUT( None ) -> None
CS_PIN_SET( NEW_PIN ) -> None
CS_PIN_CHANGE( PIN, NEW_PIN ) -> None
CS_PIN_ATTEMPTS( None ) -> counter
CS_FACTORY_RESET( None ) -> None
CS_BACKUP_READ( SLOTID ) -> {SLOTID, IV, HMAC, DATA}
CS_BACKUP_WRITE( {SLOTID, IV, HMAC, DATA} ) -> None
CS_BACKUP_BEGIN( PASS, SALT ) -> SALT
CS_BACKUP_FINISH( None ) -> None
CS_ACTIVATION_BEGIN( None ) -> NONCE, SN
CS_ACTIVATION_FINISH( SIGNATURE, STATE ) -> None
CS_GET_RANDOM( None ) -> RANDOM

Example Usage

async function test_read_write(){
    await CS_LOGIN('12345678');
    await CS_WRITE({ID='record ID', DATA='data'});
    const read_data = await CS_READ({ID='record ID'});
    await CS_LOGOUT();
    return read_data;
}

Low-Level API

Low level API consist of two functions - for sending and receiving - namely:

  • async function cs_device_receive(cmd) -> data_received.

  • async function cs_device_send(cmd, data_to_send);

where:

  • cmd is the command id;

  • data_to_send is the CBOR structure to send;

  • data_received is the CBOR structure received from the device.

Underneath these two use standard FIDO WebAuthn API function to send data: navigator.credentials.get

In the example used below, error checks are skipped for clarity

Reading CS Memory Status

async function storage_status(){
    let empty_arr = new Uint8Array(1).fill(65);
    await cs_device_send(CS_CMD.FREE, empty_arr);
    let response_cbor = await cs_device_receive(CS_CMD.FREE);
    let response = CBOR.decode(response_cbor.buffer);
    return response;
}

Writing Data Record

async function storage_test_record_write(){
    const data = {ID: getBinaryStr("AAAA1"),
    Data: getBinaryStr("CCCCCC")};
    const ID_arr = {ID: getBinaryStr("AAAA1")};
    await cs_device_send(CS_CMD.TEST_CLEAR,
    CBOR_encode_uint8t({}));
    const succesful_write = await cs_device_send(CS_CMD.WRITE,
    CBOR_encode_uint8t(data));
    const read_cbor_data_succ = await cs_device_send(CS_CMD.READ,
    CBOR_encode_uint8t(ID_arr));
    const read_cbor_data = await cs_device_receive(CS_CMD.READ,
    CBOR_encode_uint8t(ID_arr));
    const read_data = CBOR.decode(read_cbor_data.buffer);
    return read_data;
}

Flash Layout

Whole device’s flash layout is as following:

where:

  • ‘Bootloader’ is an application described in the Bootloader chapter;

  • ‘Bootloader data’ is where the latest used firmware version stored, and potential future data usable for the bootloader;

  • ‘Application’ is the main application run on the device, providing FIDO U2F, FIDO2 and CS features;

  • ‘CS state’ means memory reserved for additional data structures, supporting access to the CS;

  • ‘CS’ here means Custom Storage data - here all the CS records are stored in the encrypted form;

  • ‘User data’ is FIDO U2F / FIDO2-related user data, as well as user PIN and the CS’ AES master key in the encrypted form.

All the ranges in the table are provided as [x,y], which means that the range starts from x inclusively, and uses all bytes until y exclusively.

State Structure

The main STATE structure (which includes e.g. the CS AES master encryption key) is saved at 2 last MCU’s FLASH pages in the user data region:

#define PAGES 128
#define STATE1_PAGE (PAGES - 1)
#define STATE2_PAGE (PAGES - 2)

STATE is a general application structure for the user data, and should not be mistaken with the CS state. Its type - AuthenticatorState - is defined as follows:

typedef struct
{
    // Pin information
    uint8_t is_initialized;
    uint8_t is_pin_set;
    uint8_t pin_code[NEW_PIN_ENC_MIN_SIZE];
    int pin_code_length;
    int8_t remaining_tries;
    uint16_t rk_stored;
    uint16_t key_lens[MAX_KEYS];
    uint8_t key_space[KEY_SPACE_BYTES];
    uint8_t PIN_SALT[PIN_SALT_LEN];
    uint8_t PKBDF2_SALT[32];
    storage_master_key_t storage_master_key_enc;
    uint8_t storage_master_key_set;
    uint8_t is_custom_feature_activated;
} AuthenticatorState;

Internal Storage Layout

Custom Storage (CS) data are kept in the user data memory of the MCU flash. Currently 20 pages in range [-35, -15) are occupied by the data slots (where negative numbers mean pages index counting from the last).

Data Slots Count

Each page on STM32L432 flash takes 2048 bytes, which for 20 pages gives 40 kB. Structure describing data slot, ext_storage_record, takes 512 bytes, which yields total 80 slots to write. This could be potentially further extended.

Storage Extension

At the moment firmware takes about 122 kB out of 256 kB. FIDO2 user data takes about 30 kB. This makes it possible to configure the storage to use up to 104 kB (256-122-30). With 512 bytes data slot size this allows to store up to 208 data slots, or increase the current slots maximum size to 1024 kB.

Data Slot Composition

Data slot structure consist of two fields:

  • data[512 - 16] - to store the actual user data;

  • origin[16] - to store the first 16 bytes of the SHA256 hash of the origin of request to store the data record. This will later be compared before the access to the slot.

Full C structure:

typedef struct ext_storage_record {
    union {
    uint8_t data[512 - 16];
    uint32_t empty_marker;
};
uint8_t origin[16];
} ext_storage_record;

Data Slot Content

The data field contains all the CBOR (description below) supplied data as-is.

For data slots, the only required CBOR field is ID, which cannot be longer than FIELD_SIZE_ID = 100 bytes (size could be changed in ext_storage.h).

The rest of the CBOR map could contain any named fields, which are not containing reserved names (ones prefixed with _). Whole field will be read back on the request.

Example CBOR encoded data:

  • human-readable representation: {ID=b'this is ID', DATA1=b'any binary data', date=b'2019-07-01'}

  • CBOR: b'\xa3bIDJthis is IDddateJ2019-07-01eDATA1Oany binary data'

  • CBOR hex: a36249444a7468697320697320494464646174654a32303139 2d30372d30316544415441314f616e792062696e6172792064 617461

CBOR

CBOR is a data encoding method, which can be seen as a lower-level JSON.

By the definition from the official site, https://cbor.io, it is:

“The Concise Binary Object Representation (CBOR) is a data format whose design goals include the possibility of extremely small code size, fairly small message size, and extensibility without the need for version negotiation.”

More details on the main site, or in the RFC document: https://tools.ietf.org/html/rfc7049

Cross-Origin Read Protection

Just after the decryption, before returning the data for further processing, the origin of the data record is compared against current one.

If the origin mismatch, the decrypted data is removed from RAM, and the low-level read function signalize this specific data slot is used, but inaccessible.

Having this check in the lowest access level guarantees, that all commands will operate only on the data sourced from the same origin, as the current one.

Last updated