Trusted firmware secure services; Nonsecure client; Configuration; TF-M client operation; TF-M client test; Protected storage; Internal trusted storage; Attestation service; Audit service; Cryptography service; mbedCrypto
In this chapter, we will examine the TF-M security services and the API calls available to the Non-Secure application code, but first we will look at the TF-M Non-Secure client, which is used to pass the API calls across the isolation boundary.
The Non-Secure code contains a client and API definitions that are used to communicate with the Secure Partition Manager. The client supports both the Secure Function (SFN) call and the Inter-Process Calling (IPC) methods. Whichever calling method is used, an API function is first used to populate a structure with the service ID and the passed parameters before making a call across the isolation boundary.
The Non-Secure code must be configured with the TF-M client by selecting the TFM::Core option in the RTE and selecting the calling type to match the configuration of the Secure partition code.
Next, we must add the support files for each of the mandatory services that will be used by the Non-Secure code (Fig. 13.1). If you are using the Attestation service, you will also need to add the QCBOR and T_COSE libraries. The T_COSE encryption type must be set to match the configuration of the attestation service in the Secure Partition.
Once the files have been added to the RTE, you must include the Non-Secure interface header file in the application code.
We can access a service in the Secure partition by adding the service header file, for example, the cryptography API.
Each of the services provide a client API function, which can be called directly from the application code.
The operation structure is declared and initialized prior to calling the API function.
psa:hash_operation_t operation = PSA_HASH_OPERATION_INIT;
The NS API function will populate a structure with the Secure Function ID of the required service, and a handle used to manage the call:
struct tfm_crypto_pack_iovec iov = { .sfn_id = TFM_CRYPTO_HASH_SETUP_SID, .alg = alg, .op_handle = operation->handle, };
Then, place the calling parameters into input and output IO vector buffers, which are used to transfer data across the isolation boundary:
psa:invec in_vec[] = { {.base = &iov, .len = sizeof(struct tfm_crypto_pack_iovec)}, }; psa:outvec out_vec[] = { {.base = &(operation->handle), .len = sizeof(uint32_t)}, };
The API function then calls a dispatcher function which takes the Secure function name and ID as well as the IO vectors:
The underlying dispatcher function is specific to the TF-M calling model selected:
define API_DISPATCH(sfn_name, sfn_id) tfm_ns_interface:dispatch((veneer_fn)tfm_##sfn_name##_veneer, (uint32_t)in_vec, ARRAY_SIZE(in_vec), (uint32_t)out_vec, ARRAY_SIZE(out_vec))
The tfm_ns_interface:dispatch() dispatcher function will use an RTOS mutex to lock the critical code.
The API call structure is sent to the secure veneer function. The veneer function uses a supervisor call to enter the processor privileged mode prior to entering the secure partition. It will also add the partition ID and pass the API call structure to the Secure Partition Manager (SPM).
result = fn(arg0, arg1, arg2, arg3);
Once the message has been processed, the mutex is released.
os_wrapper_mutex_release(ns_lock_handle)
The IPC calling method used the same initial function to add the SID and prepare the IOVEC buffers. However, to send the API call, it then uses the connectcalldisconnect functions to communicate with the SPM and the endpoint security service.
Once the IO vectors have been populated, the NS Client will connect to the secure partition via the SPM.
Once again the dispatcher function will be called:
status = API_DISPATCH(tfm_crypto_hash_setup,TFM_CRYPTO_HASH_SETUP);
However, this time it is aliased to support the IPC communication model and will generate a psa:call() to the secure partition.
#define API_DISPATCH(sfn_name, sfn_id)psa:call(ipc_handle, PSA_IPC_CALL, in_vec, ARRAY_SIZE(in_vec), out_vec, ARRAY_SIZE(out_vec))
The call function will place the size of the IOVEC buffers in the ctrl_params structure and then call the original dispatcher function.
tfm_ns_interface:dispatch( (veneer_fn)tfm_psa:call_veneer, (uint32_t)handle, (uint32_t)&ctrl_param, (uint32_t)in_vec, (uint32_t)out_vec);
When the transaction has finished, the connection to the secure partition is closed.
The Secure Partition Trusted Firmware provides the PSA mandatory security services cryptography, storage, auditing, attestation, and firmware update. Each service has an init function that is invoked on startup and does not need to be part of the application code.
Each security service provides a header file that defines an API and a return value. As part of our secure coding practice, any application code should always check the return value. The return value is of the type:
Ideally, you should get PSA_SUCCESS as a result. If the function fails, there are a wide range of error values that should be logged for future diagnostic use. The full range of error codes can be found in the header file.
After any operation with the secure services, you must also wipe any associated memory and variables in the Non-Secure code, including data stored on the stack or heap. Each security service provides an abort function which should be called if the results of a secure function are no longer required. You should also destroy any keys that are no longer in active use.
The Trusted Firmware provides two storage volumes in the Secure Partition. These are Protected Storage (PS) and Internal Trusted Storage (ITS). Protected Storage is intended to provide storage for application “data at rest” and provides protection against software and hardware attack. The Internal Trusted Storage volume provides the highest level of security for device intimate data such as cryptographic keys, firmware measurements, and other device secrets.
The Protected Storage volume will generally be a region of the microcontroller FLASH though it could be an external volume such as serial FLASH memory. The PS storage volume can be located in either the Secure or Non-Secure partition through PSA storage functions are always located in the Secure Partition. The Protected Storage is different from the other services in that it is run as a service within the Application RoT rather than the PSA RoT. The protected storage is capable of providing confidentiality, integrity, and replay protection. However, depending on the threat model and user requirements, data may be stored without confidentiality or replay protection, while data integrity is always validated.
The protected storage volume is accessed through an API provided to the Non-Secure code by adding the header file “psa/protected_storage.h” to the application Non-Secure code. This provides the following storage API (Table 13.1):
Function | Description |
---|---|
psa:ps_get | Retrieve data associated with a provided UID |
psa:ps_set | Create a new or modify an existing key/value pair |
psa:ps_get_info | Retrieve the metadata about the provided UID |
psa:ps_remove | Remove the provided UID and its associated data from the storage |
psa:ps_create | Reserves storage for the specified UID. |
psa:ps_set_extended | Sets partial data into an asset based on the given identifier |
psa:ps_get_support | Returns a bitmask with flags set for all the optional features supported by the implementation |
When storing data, additional flags are used to control the security settings for a given data item. While confidentiality and anti-replay are optional, integrity is always applied.
In the case of public data, it is possible to disable encryption:
PSA_STORAGE_FLAG_NO_CONFIDENTIALITY
Similarly, we can also disable replay protection if required.
PSA_STORAGE_FLAG_NO_REPLAY_PROTECTION
We can also store an item as a constant so that one it has been written it cannot be updated. Such an immutable item can be created with the following flag.
Data items are stored as key value pairs, which are referenced through a Unique Identifier (UID) using the type:
psa:storage_uid_tPS_Block = 0x01;
We can store a data item as follows:
psa:status_t res ; size_t PS_BlockSize = 20; uint8_t PS_Blockdata[10] = "1234567890"; psa:storage_create_flags_t PS_CreateFlags = 0; res = psa:ps_set( PS_Block, PS_BlockSize, PS_Blockdata, PS_CreateFlags);
When data are stored, depending on the implementation, the modifying flags may be ignored. For example, you may request no confidentiality, but the storage service may be configured to always encrypt the data. We can check the actual storage state using the following API call:
which will return the capacity of the storage, the size of the data currently stored, and any flags that were set during creation.
If you need to create a storage item but only want to store part of the record, you must first create the item and then use the psa:ps_set_extended() function to add the data.
size_t PS_DataSize = 10; size_t PS_Offset = 10; If( psa:ps_get_support() == PSA_STORAGE_SUPPORT_SET_EXTENDED ) { res = psa:ps_create(PS_Block, PS_BlockSize, PS_CreateFlags);res = psa:ps_set_extended(PS_Block,PS_Offset, PS_DataSize,PS_Blockdata ); }
Data items can be retrieved or partially retrieved in a similar fashion and if necessary deleted.
This exercise uses the Protected Storage API to save and retrieve data to the protected storage volume.
The protected storage API is enabled as an encrypted volume.
Examine the configuration options for the protected storage volume (Fig. 13.2).
We can control the FLASH volume by selecting an appropriate CMSIS-FLASH driver. It is also possible to enable rollback protection and control the maximum item storage size and number of items that can be saved to the protected storage volume.
The “Create Flash layout” option will force the secure service to format the FLASH volume and erase all existing data.
Validate FLASH Metadata will force the secure service to check the item metadata during a read operation to ensure that it is not corrupted or subject to a malicious change.
If you change any of these options, you must also consider how any change to the size of the protected storage volume will impact the memory map defined in the original CMSIS Zone project.
The app-main thread uses each of the Protected Storage functions to save, retrieve and extend blocks of data into the secure FLASH volume.
The Internal Trusted Storage is used to store a small number of high-value secrets and consequently always provides confidentiality, integrity, and replay protection. The Internal Trusted Storage is run as a service within the PSA Root of Trust, and the data will be stored in a small volume located in the internal secure FLASH memory. The API is defined by including the header file:
"psa/internal_trusted_storage.h"
and contains the following functions (Table 13.2):
Function | Description |
---|---|
psa:its_get | Reads a data entry |
psa:its_set | Creates a data entry and saves the application data |
psa:its_get_info | Returns data entry meta data (size and capacity) in the form of psa:storage_info_t |
psa:its_get_remove | Deletes the data entry and frees the storage block |
The Internal Trusted Storage functions operate with a similar but more limited API compared to the protected storage functions. When a data item is stored in the internal protected memory, the NO_CONFIDENTIALITY and PSA_STORAGE_FLAG_NO_REPLAY_PROTECTION have no effect, while the STORAGE_FLAG_WRITE_ONCE flag will always work.
We can now store secrets in the Internal Trusted Storage.
psa:status_t psa:storage_create_flags_t ITS_CreateFlags = 0; psa:storage_uid_t ITS_Block = 0x01; size_t ITS_BlockSize = 20; uint8_t ITS_Blockdata[10] = "1234567890"; uint8_t ITS_Read_Blockdata[10]; size_t ITS_Read_Size = 0; res = psa:its_set(ITS_Block,ITS_BlockSize, ITS_Blockdata, ITS_CreateFlags); res =psa:its_get(ITS_Block,0, 10, ITS_Read_Blockdata, &ITS_Read_Size); psa:its_remove(ITS_Block);
In this exercise, we will use the Internal Trusted Storage to store and retrieve a block of data.
The Internal Trusted Storage API is enabled and defaults to an encrypted volume with rollback protection.
Like the protected storage, we can define the maximum storage size for each volume of data and the number of items that can be stored in the volume. We can also access the FLASH storage volume by selecting the configured CMSIS-FLASH driver.
The app-main thread uses each of the Internal Trusted Storage functions to save retrieve and extend blocks of data into the secure FLASH volume.
In the cryptography chapters, we saw how to use different algorithms by creating projects with a flat memory model that can be used with any Arm-based microcontroller. With the introduction of TrustZone, in order to create a secure application, we need to place the low-level cryptographic algorithms and their associated secrets in the secure partition. To solve this problem, a separate library called mbedCrypto has been created. The mbedCrypto library provides a set of abstraction layers for each group of algorithms. This allows us to place the sensitive functions in the Secure partition while the mbedTLS code responsible for the TLS protocol can remain in the Non-Secure partition (Fig. 13.4). It is likely that more functionality within mbedTLS will migrate to mbedCrypto over time.
The mbedCrypto library provides abstraction layers for the following groups of algorithms (Table 13.3):
Abstraction layer |
---|
Message DIGEST |
Message authentication codes |
Symmetric cipher |
Key derivation |
Random number |
Key management |
Asymmetric encryption |
Hash and sign |
Key agreement |
The symmetric cipher, MAC, and message digest abstraction layers may be used with a single block of data or are capable of working with large blocks of data or streaming data. A multipart operation consists of an initial setup function followed by a call to an update function. Once the data have been processed, the session can be terminated, and if appropriate, a final result can be calculated with a finish function.
To access the mbedCrypto functions, you must include the header “psa/crypto.h”.
The mbedCrypto service provides a random function that are used to provide keying material and streams of random values (Table 13.4).
We can fill a local buffer with random data as follows.
In addition to the core cryptographic algorithms, mbedCrypto provides a rich API to create and manage encryption keys. By default, the Internal Trusted Storage is used to hold the encryption keys, but the API can be modified to target a secure key store provided by the underlying microcontroller. When using the key management API, the Non-Secure code and other security services reference the keys by identifiers, which are defined during the key generation process. When created, a key may be given a range of attributes and usage policies that allow it to be used by the Non-Secure code and/or other secure services. Each key is given a lifetime, which allows it to be persistent and be stored in the ITS until it is destroyed, or it may be a volatile key that has a lifetime limited to the current session. We can also control if a key is allowed to leave the Secure World by defining it as extractable or nonextractable. As an example, both a public and private key may be held in the Internal Trusted Storage. The private key would be labeled as nonextractable, while the public key would be defined as extractable. Once a key has been generated, an additional set of functions are used to manage its lifecycle, as shown in Table 13.5.
Function | Description |
---|---|
psa:import_key | Import a key in binary format |
psa:generate_key | Generate a key or key pair |
psa:copy_key | Make a copy of a key |
psa:purge_key | Remove nonessential copies of key material from memory |
psa:destroy_key | Destroy a key |
The capabilities of a key are stored in a structure psa:key_attributes_t. This structure contains a set of elements that define the following features (Table 13.6).
Attribute | Description |
---|---|
ID | The Key ID |
Lifetime | Whether the key is persistent or volatile |
Type | The key type and its algorithm |
Keysize | The key size in bits |
Usage flags | Allowed operations |
Algorithm | The algorithms that may be used by the key |
Location | If available, you can select additional storage volumes |
When a key is created, we pass a unique integer value of type psa:key_id_t. This becomes the key ID that is used by the Non-Secure code to reference the key for all future operations. When a key is defined, we must also specify its usage policy as a set of flags (Table 13.7), which algorithms it can be used with and its bit size.
Usage flag | Description |
---|---|
PSA_KEY_USAGE_EXPORT | Permission to export the key |
PSA_KEY_USAGE_COPY | Permission to copy the key |
PSA_KEY_USAGE_CACHE | Permission for the implementation to cache the key |
PSA_KEY_USAGE_ENCRYPT | Permission to encrypt a message with the key |
PSA_KEY_USAGE_DECRYPT | Permission to decrypt a message with the key |
PSA_KEY_USAGE_SIGN_MESSAGE | Permission to sign a message with the key |
PSA_KEY_USAGE_VERIFY_MESSAGE | Permission to verify a message signature with the key |
PSA_KEY_USAGE_SIGN_HASH | Permission to sign a message hash with the key |
PSA_KEY_USAGE_VERIFY_HASH | Permission to verify a message hash with the key |
PSA_KEY_USAGE_DERIVE | Permission to derive other keys from this key |
The Non-Secure code does not have access to the key and its attributes directly but can manage the key, and its attributes through a set of helper functions (Table 13.8). We again define the range of algorithms the key can be used for. Although this is already defined in key type, this second entry defines additional information necessary to use the algorithm, such as padding type.
Function | Description |
---|---|
psa:set_key_id | Declare a key as persistent and set its key identifier |
psa:get_key_id | Retrieve the key identifier from key attributes |
psa:set_key_lifetime | Set the lifetime of a persistent key |
psa:get_key_lifetime | Get the lifetime of a persistent key |
psa:set_key_usage_flags | Declare usage flags for a key |
psa:get_key_usage_flags | Retrieve the usage flags from key attributes |
psa:set_key_algorithm | Declare the permitted algorithm policy for a key |
psa:get_key_algorithm | Retrieve the algorithm policy from key attributes |
psa:set_key_type | Declare the type of a key |
psa:get_key_type | Retrieve the key type from key attributes |
psa:set_key_bits | Declare the size of a key |
psa:get_key_bits | Retrieve the key size from key attributes |
The PSA crypto library also provides a set of high-level functions, which can be used to initialize, read, and reset the key attributes as a whole (Table 13.9). The reset function should be called after any operations on the key to wipe the Non-Secure attribute memory.
Function | Description |
---|---|
psa:key_attributes_init | Return an initial value for a key attribute object |
psa:get_key_attributes | Retrieve the attributes of a key |
psa:reset_key_attributes | Wipe the key attribute object to a freshly initialized state |
Once a key has been created, it is stored in the Internal Trusted Storage. Within the Non-Secure code, it is then referenced by its ID. We can now use these functions to create a set of keys and store them in the ITS. First, declare and initialize the attributes object and the key ID:
To manage our key, we can create a key ID value:
#define TEST_KEY_ID (psa:key_id_t) 1
This is stored in the attributes along with other the key parameters and policies.
psa:set_key_id(&attributes,TEST_KEY_ID) psa:set_key_usage_flags(&attributes,PSA_KEY_USAGE_ENCRYPT PSA_KEY_USAGE_DECRYPT );
psa:set_key_algorithm(&attributes, 0); psa:set_key_type(&attributes, PSA_KEY_TYPE_AES); psa:set_key_bits(&attributes, 128); psa:set_key_lifetime(&attributes, PSA_KEY_LIFETIME_PERSISTENT);
Now we can store an existing key with psa:key_import() or generate a symmetrical key using a random process in the secure service.
status = psa:generate_key(&attributes, &test_key_id);
Once the key has been generated, the returned test_key_id will equal original define TEST_KEY_ID.
Once we have a set of keys, we can start to use the mbedCrypto algorithms.
For some algorithms, we will need to create ephemeral session keys. The mbedCrypto library provides a set of key derivation functions to manage these operations (Table 13.10).
Function | Description |
---|---|
psa:key_derivation_operation_init | Return an initial value for a key derivation operation object |
psa:key_derivation_setup | Set up a key derivation operation |
psa:key_derivation_get_capacity | Retrieve the current capacity of a key derivation operation |
psa:key_derivation_set_capacity | Set the maximum number of bytes the derivation can return |
psa:key_derivation_input_bytes | Provide an input for key derivation or key agreement |
psa:key_derivation_input_key | Provide an input for key derivation in the form of a key |
psa:key_derivation_key_agreement | Perform a key agreement using a shared secret as input |
psa:key_derivation_output_bytes | Read some data from a key derivation operation |
psa:key_derivation_output_key | Derive a key from an ongoing key derivation operation |
psa:key_derivation_abort | Abort a key derivation operation |
psa:raw_key_agreement | Perform a key agreement and return the raw shared secret |
First create and initialize the key objects:
psa:key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; psa:key_derivation_operation_t operation = PSA_KEY_DERIVATION_OPERATION_INIT;
We must also create a salt to prevent lookup attacks:
const unsigned char salt[] = {0x1,0x2,0x3,0x4};
We can also select the underlying algorithm used for the derivation:
psa:algorithm_t alg = PSA_ALG_HKDF(PSA_ALG_SHA_256);
then define the characteristics of the required key:
and then define the id’s used to manage an existing and derived key:
To derive the key, we can setup the key derivation algorithm and its bit size capacity:
status = psa:key_derivation_setup(&operation, alg); status = psa:key_derivation_set_capacity(&operation, capacity);
Then, add the keying material:
status = psa:key_derivation_input_bytes(&operation, PSA_KEY_DERIVATION_INPUT_SALTsalt, sizeof(salt)); status = psa:key_derivation_input_key(&operation, PSA_KEY_DERIVATION_INPUT_SECRET, test_key_aes); status = psa:key_derivation_input_bytes(&operation, PSA_KEY_DERIVATION_INPUT_INFO, info,sizeof(info));
Then, define the key attributes:
psa:set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT); psa:set_key_algorithm(&attributes, PSA_ALG_CTR); psa:set_key_type(&attributes, PSA_KEY_TYPE_AES); psa:set_key_bits(&attributes, 128);
Finally, derive the key:
The mbedCrypto service provides an abstraction layer for any symmetrical ciphers that have been enabled in the secure service (Table 13.11).
Function | Description |
---|---|
psa:cipher_encrypt | Encrypt a message using a symmetric cipher |
psa:cipher_decrypt | Decrypt a message using a symmetric cipher |
psa:cipher_operation_init | Return an initial value for a cipher operation object |
psa:cipher_encrypt_setup | Set the key for a multipart symmetric encryption operation |
psa:cipher_decrypt_setup | Set the key for a multipart symmetric decryption operation |
psa:cipher_generate_iv | Generate an Initialization Vector (IV) |
psa:cipher_set_iv | Set the Initialization Vector (IV) |
psa:cipher_update | Encrypt or decrypt a message fragment in an active cipher operation |
psa:cipher_finish | Finish encrypting or decrypting a message in a cipher operation |
psa:cipher_abort | Abort a cipher operation |
To use a symmetrical cipher, we can first setup the attributes objects and define the chaining method:
psa:status_t status;psa:key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;psa:algorithm_t alg = PSA_ALG_CBC_NO_PADDING;uint8_t plaintext[block_size] = “Hello World”; uint8_t iv[block_size]; size_t iv_len; uint8_t output[block_size]; size_t output_len; psa:key_handle_t handle; psa:cipher_operation_t operation = PSA_CIPHER_OPERATION_INIT;
Now we can open a test key and setup the cipher for multipart operation:
status = psa:cipher_encrypt_setup(&operation,test_key_aes, alg);
Before the first packet of plaintext, we must generate an IV:
Then we can iterate through the data:
status = psa:cipher_update(&operation,plaintext, sizeof(plaintext), output, sizeof(output), &output_len);
Until we finish with the last packet:
The AEAD algorithm is also provided as a set of API functions (Table 13.12).
Function | Description |
---|---|
psa:aead_encrypt | Process an authenticated encryption operation |
psa:aead_decrypt | Process an authenticated decryption operation |
psa:aead_operation_init | Return an initial value for an AEAD operation object |
psa:aead_encrypt_setup | Set the key for a multipart authenticated encryption operation |
psa:aead_decrypt_setup | Set the key for a multipart authenticated decryption operation |
psa:aead_generate_nonce | Generate a random nonce for an authenticated encryption operation |
psa:aead_set_nonce | Set the NONCE for an authenticated encryption or decryption operation |
psa:aead_set_lengths | Declare the lengths of the message and additional data for AEAD |
psa:aead_update_ad | Pass additional data to an active AEAD operation |
psa:aead_update | Encrypt or decrypt a message fragment in an active AEAD operation |
psa:aead_finish | Finish encrypting a message in an AEAD operation |
psa:aead_verify | Finish authenticating and decrypting a message in an AEAD operation |
psa:aead_abort | Abort an AEAD operation |
We can use the AEAD algorithm to perform a simple encryption of a block of data. Here, we must provide the key ID, a NONCE, which must be unique for each session, the unencrypted associated data, and the input data, which will be encrypted:
status = psa:aead_encrypt(test_key_aead,PSA_ALG_CCM, nonce, sizeof(nonce), additional_data, sizeof(additional_data), input_data, sizeof(input_data), output_data, output_size, &output_length);
The same key and NONCE is used in the decryption function.
status = psa:aead_decrypt(test_key_aead,PSA_ALG_CCM, nonce, sizeof(nonce), additional_data, sizeof(additional_data), input_data, sizeof(input_data), output_data, output_size, &output_length);
We can also perform a multipart operation in a similar fashion to the symmetrical cipher above.
The mbedCrypto library also provides an abstraction layer for the mbedTLS hashing algorithms (Table 13.13).
Function | Description |
---|---|
psa:hash_compute | Calculate the hash (digest) of a message |
psa:hash_compare | Calculate the hash (digest) of a message and compare it with a reference value |
psa:hash_operation_init | Return an initial value for a hash operation object |
psa:hash_setup | Set up a multipart hash operation |
psa:hash_update | Add a message fragment to a multipart hash operation |
psa:hash_finish | Finish the calculation of the hash of a message |
psa:hash_verify | Finish the calculation of the hash of a message and compare it with an expected value |
psa:hash_abort | Abort a hash operation |
psa:hash_suspend | Halt the hash operation and extract the intermediate state of the hash computation |
psa:hash_resume | Set up a multipart hash operation using the hash suspend state from a previously suspended hash operation |
psa:hash_clone | Copy the state of an ongoing hash operation to a new operation object |
We can perform a multipart hashing operation by first setting up the hash operation with the selected hash algorithm:
status = psa:hash_setup(&operation, alg);
Then we can apply packets of the data:
status = psa:hash_update(&operation, input, sizeof(input));
Until we reach the end of the data block when we can generate the final hash:
Or verify against an existing hash:
A set of MAC functions are also available. The base hashing algorithm can be selected at the start of the MAC operation (Table 13.14).
Function | Description |
---|---|
psa:mac_compute | Calculate the Message Authentication Code (MAC) of a message |
psa:mac_verify | Calculate the MAC of a message and compare it with a reference value |
psa:mac_sign_setup | Set up a multipart MAC calculation operation |
psa:mac_verify_setup | Set up a multipart MAC verification operation |
psa:mac_update | Add a message fragment to a multipart MAC operation |
psa:mac_sign_finish | Finish the calculation of the MAC of a message |
psa:mac_verify_finish | Finish the MAC calculation and compare it with an expected value |
psa:mac_abort | Abort a MAC operation |
Like the hash algorithms, we can MAC a single packet of data or setup a multipart operation.
The mbedCrypto library also supports a range of asymmetric algorithms for encryptiondecryption and signing of data (Table 13.15). The abstraction layer supports both the RSA cryptosystem and the Diffie-Hellman key agreement algorithm. Signing can also be accomplished with RSA, DSA, or ECDSA.
Function | Description |
---|---|
psa:asymmetric_encrypt | Encrypt a short message with a public key |
psa:asymmetric_decrypt | Decrypt a short message with a private key |
psa:sign_message | Hash and sign a message with a private key |
psa:verify_message | Verify the signature and hash of a message with a public key |
psa:sign_hash | Sign an already-calculated hash with a private key |
psa:verify_hash | Verify the signature of a hash or short message using a public key |
Here we can sign a message using an already calculated hash. The key will define the algorithm though we must also specify a padding method:
The Key Agreement API allows us to calculate a shared secret by combining our key with the public key from a peer (Table 13.16). We can also calculate the secret and derive a symmetric encryption key with a single API call.
Function | Description |
---|---|
psa:raw_key_agreement | Perform a key agreement and return the raw shared secret |
psa:key_derivation_key_agreement | Perform a key agreement and use the shared secret as input to a key derivation |
Once we exchange public keys with a peer, it is possible to calculate a shared secret.
The mbedCrypto Service is designed to run in the secure partition and provide essential cryptography algorithms to the nonsecure code. Mbed Crypto also provides a key storage and management API that allows secrets to be placed within the ITS where they can be referenced by the nonsecure code through ID’s. Additionally, the keys may be bound to a specific service and function.
Here the nonsecure crypto service is enabled. This adds the client and the mbedCrypto.h header file.
In the secure project open TFM::tfm_config.h (Fig. 13.5).
This allows us to define how much memory is available to the cryptography service along with the size of the IO vectors used to pass data to and from the service. We can also disable unwanted algorithms.
The mbedCrypto service acts as an Oracle where the nonsecure code can request a cryptography service without any knowledge of the encryption key.
An attestation service is used to create a token that is used to prove its identity and capabilities. The token can then be used to access resources on a server in place of a formal login. Existing web token schemes based on CBOR and JSON encoding are currently available to access HTTPS resources. However, these schemes do not fully meet the requirements of an IoT device. A new attestation token scheme called Entity Attestation Token (EAT) is used within the PSA Trusted firmware, which provides sufficient flexibility for IoT devices.
An Entity Attestation Token is a means for an IoT device to identify itself to a server and describe its characteristics as a set of security claims. A token is a blob of binary data, which is signed using the ECDSA algorithm to provide a guarantee of authentication. Typically a token will be encoded using CBOR and then encrypted and signed. While a token will generally carry fixed security claims, it may also be used to transfer a limited amount of process data. A good example of this would be “Number of units consumed” in a metering application.
A token will encode a number of security claims that are used to describe its origin and capabilities to the server. The Internet Assigned Numbers Authority maintains a registry of claims which may be used as templates for any given application. If a suitable token is not available, you can create your own security claims and token description. The format shown below is currently used (Table 13.17).
Attestation fields | Description |
---|---|
Boot seed | A 32bit random value generated at startup |
Challenge | A block of user supplied data |
Implementation ID | The device PSA implementation ID |
Instance ID | The device Instance ID |
Profile ID | Profile definition, currently defaults to PSA_IOT_PROFILE_1 |
Security lifecycle | The device current lifecycle state |
Measurement description | The Hash Algorithm used for the SW component measurements |
SW components | A set of claims for each Software component |
A claim is then added for each software component, typically this will be the Application Image, Trusted Firmware, and the PSA bootloader. Each claim is made up of the measurements shown in Table 13.18.
SW component field | Description |
---|---|
Measurement value | Hash of the SW component |
Signer ID | ID of the signing entity |
SW component | Name of the SW component |
SW COMPONENT version | The Version number in Major.Minor format |
The token data will be encoded as a CBOR object and then will be encrypted and signed in a “CBOR Object Signing and Encryption” COSE format.
Alongside the token, a device will create an ECDSA key pair. The private key will be stored in the device itself, and the public key will be stored in a database that is part of an attestation server (Fig. 13.6).
The attestation server may be a public service or a private part of your system. When an IoT device wishes to communicate with a server, it first sends its token to the server. The server does not read the token but blindly passes the token to the attestation verification service for authentication using the instance ID to identify the matching public key. The attestation server will return a validation result to the server. If the token is found to be valid, the server can examine the claims made in the token and make a decision to grant the service request made by the IoT device (Fig. 13.7).
The header file “psa/initial_attestation.h” provides the Non-Secure code with an API to access the Attestation token and public key. The available functions are shown in Table 13.19.
Function | Description |
---|---|
psa:initial_attest_get_token | Retrieve the Initial Attestation Token |
psa:initial_attest_get_token_size | Calculate the size of an Initial Attestation Token |
tfm_initial_attest_get_public_key | Get the attestation public key and associated elliptic curve |
The Non-Secure code can create a token by providing a block of challenge data and then requesting the token.
#define TEST_TOKEN_SIZE (0x200) #define TEST_CHALLENGE_OBJ_SIZE (32u) static uint8_t token_buffer[TEST_TOKEN_SIZE]; static uint8_t challenge_buffer[TEST_CHALLENGE_OBJ_SIZE] = {//32 bytes of user data}; uint32_t token_size; attest_err = psa:initial_attest_get_token_size(TEST_CHALLENGE_OBJ_SIZE, &token_size); attest_err = psa:initial_attest_get_token(challenge_buffer, TEST_CHALLENGE_OBJ_SIZE, token_buffer, &token_size);
In this exercise, we will create and validate an Entity Attestation Token (EAT).
The Attestation API is enabled along with the crypto service
The asymmetric key ID is the SHA256 hash of the attestation public key. This is used by the attestation verification service to retrieve the correct public key from its database.
The “Include Optional Claims” selection will be removed from the final release.
The attestation API is used to first get the size of a token and then generate the token before storing it onto the SD card. To generate the token we provide a challenge in the form of a block of random data. The challenge is included within the token to prove it has been freshly created.
We can now validate the token using a set of Python scripts provided by the TFM component pack.
<PATH>Keil_v5ARMPACKARMTFM<version> oolsiat-verifier
pip3 install . (note there is a dot (.) at the end of this command)
python check_iat <path>/token.cbr
This will give a pass or fail on the Token format.
check_iat -k <path>/key.pem <path>/token.cbr
decompile_token -k <path>/key.pem <path>/token.cbr
The PSA audit functions provides a way for the secure code to write audit log messages, which can be retrieved by the Non-Secure code.
The secure services and partition manager can write messages to the audit log using the API shown in Table 13.20.
The Non-Secure code can monitor and read the audit log (Table 13.21) using the header file “psa/initial_audit_api.h”.
Function | Description |
---|---|
psa:audit_retrieve_record | Retrieves a record at the specified index |
psa:audit_get_info | Returns the total number and size of the records stored |
psa:audit_get_record_info | Returns the size of the record at the specified index |
psa:audit_delete_record | Deletes a record at the specified index |
The Non-Secure code can read the audit log using this simple API.
The audit log is generated secure partition and accessed by the nonsecure code. The nonsecure audit client provides a minimal API to manage the audit records.
In the secure project the Audit logging is enabled in the RTE and currently has no further configuration options.
The client API provides functions to get the total number of audit records their total stored size and the size of a particular record.
We can then read and delete a specific record.
Finally the Non-Secure code can request the current lifecycle state using the API call shown in Table 13.22 and provided by the header “psa/lifecycle.h”.
Function | Description |
---|---|
psa:rot_lifecycle_state | Returns the current lifecycle state of the PSA RoT |
The lifecycle state is returned in bits [15:8] while bits [7:0] are reserved for future implementation defined states.
Once the TF-M software has been installed onto the Secure partition, it must be provisioned with all the necessary device secrets so that it fully enters into its operational lifecycle. This can be achieved during manufacture by installing a provisioning program into the NSPE. The provisioning program is used to download the necessary secrets into the SPE and the TF-M ITS storage. Table 13.23 shows the range of the TF-M device intimate data.
Secret | Type | Storage |
---|---|---|
Hardware unique key | Symmetric key | ITS or device keystore |
Initial attestation key | Asymmetric private key or symmetric key | ITS or device keystore |
Instance ID | Hash of initial attestation key | ITS or device keystore |
Implementation ID | 32Byte Platform ID | Image |
Hardware ID | EAN | Image |
TLS CA cert | X.509 | PS or Image |
TLS device cert | X.509 | PS or Image |
TLS device private key | Asymmetric private key | ITS or device keystore |
RoT key | Asymmetric public key | ITS or device keystore |
Device secure boot | X.509 public key | Device specific |
Device debug | X.509 public key | Device Specific |
In this chapter, we have seen how to communicate between the Application in the NSPE and the TF-M platform in the SPE. We have also seen the range of available services within the secure partitions and how to provision the TF-M for its operational lifetime. In the next chapter, we will look at adding the Second Stage Bootloader (BL2), which will allow us to perform essential updates over the lifetime of the device.
3.239.76.211