PSA Cryptoprocessor Driver Interface

This document describes an interface for cryptoprocessor drivers in the PSA cryptography API. This interface complements the PSA Cryptography API specification, which describes the interface between a PSA Cryptography implementation and an application.

This specification is work in progress and should be considered to be in a beta stage. There is ongoing work to implement this interface in Mbed TLS, which is the reference implementation of the PSA Cryptography API. At this stage, Arm does not expect major changes, but minor changes are expected based on experience from the first implementation and on external feedback.

Time-stamp: “2020/08/05 20:37:24 GMT”

Introduction

Purpose of the driver interface

The PSA Cryptography API defines an interface that allows applications to perform cryptographic operations in a uniform way regardless of how the operations are performed. Under the hood, different keys may be processed in different hardware or in different logical partitions, and different algorithms may involve different hardware or software components.

The driver interface allows implementations of the PSA Crypytography API to be built compositionally. An implementation of the PSA Cryptography API is composed of a core and zero or more drivers. The core handles key management, enforces key usage policies, and dispatches cryptographic operations either to the applicable driver or to built-in code.

Functions in the PSA Cryptography API invoke functions in the core. Code from the core calls drivers as described in the present document.

Types of drivers

The PSA Cryptography driver interface supports two types of cryptoprocessors, and accordingly two types of drivers.

Requirements

The present specification was designed to fulfil the following high-level requirements.

[Req.plugins] It is possible to combine multiple drivers from different providers into the same implementation, without any prior arrangement other than choosing certain names and values from disjoint namespaces.

[Req.compile] It is possible to compile the code of each driver and of the core separately, and link them together. A small amount of glue code may need to be compiled once the list of drivers is available.

[Req.types] Support drivers for the following types of hardware: accelerators that operate on keys in cleartext; cryptoprocessors that can wrap keys with a built-in keys but not store user keys; and cryptoprocessors that store key material.

[Req.portable] The interface between drivers and the core does not involve any platform-specific consideration. Driver calls are simple C functions. Interactions between driver code and hardware happen inside the driver (and in fact a driver need not involve any hardware at all).

[Req.location] Applications can tell which location values correspond to which secure element drivers.

[Req.fallback] Accelerator drivers can specify that they do not fully support a cryptographic mechanism and that a fallback to core code may be necessary. Conversely, if an accelerator fully supports cryptographic mechanism, the core must be able to omit code for this mechanism.

[Req.mechanisms] Drivers can specify which mechanisms they support. A driver’s code will not be invoked for cryptographic mechanisms that it does not support.

Overview of drivers

Deliverables for a driver

To write a driver, you need to implement some functions with C linkage, and to declare these functions in a driver description file. The driver description file declares which functions the driver implements and what cryptographic mechanisms they support. Depending on the driver type, you may also need to define some C types and macros in a header file.

The concrete syntax for a driver description file is JSON. The structure of this JSON file is specified in the section “Driver description syntax”.

A driver therefore consists of:

How to provide the driver description file, the C header files and the object code is implementation-dependent.

Implementations should support multiple drivers.

Driver description syntax

The concrete syntax for a driver description file is JSON.

Driver description top-level element

A driver description is a JSON object containing the following properties:

Driver description capability

A capability declares a family of functions that the driver implements for a certain class of cryptographic mechanisms. The capability specifies which key types and algorithms are covered and the names of the types and functions that implement it.

A capability is a JSON object containing the following properties:

Example: the following capability declares that the driver can perform deterministic ECDSA signatures using SHA-256 or SHA-384 with a SECP256R1 or SECP384R1 private key (with either hash being possible in combination with either curve). If the prefix of this driver is "acme", the function that performs the signature is called acme_sign_hash.

{
    "functions": ["sign_hash"],
    "algorithms": ["PSA_ALG_DETERMINISTIC_ECDSA(PSA_ALG_SHA_256)",
                   "PSA_ALG_DETERMINISTIC_ECDSA(PSA_ALG_SHA_384)"],
    "key_types": ["PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_CURVE_SECP_R1)"],
    "key_sizes": [256, 384]
}

Algorithm and key specifications

Algorithm specifications

An algorithm specification is a string consisting of a PSA_ALG_xxx macro that specifies a cryptographic algorithm defined by the PSA Cryptography API. If the macro takes arguments, the string must have the syntax of a C macro call and each argument must be an algorithm specification or a decimal or hexadecimal literal with no suffix, depending on the expected type of argument.

Spaces are optional after commas. Whether other whitespace is permitted is implementation-specific.

Valid examples:

PSA_ALG_SHA_256
PSA_ALG_HMAC(PSA_ALG_SHA_256)
PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, PSA_ALG_HKDF(PSA_ALG_SHA_256))

Key type specifications

An algorithm specification is a string consisting of a PSA_KEY_TYPE_xxx macro that specifies a key type defined by the PSA Cryptography API. If the macro takes an argument, the string must have the syntax of a C macro call and each argument must be the name of a constant of suitable type (curve or group).

The name _ may be used instead of a curve or group to indicate that the capability concerns all curves or groups.

Valid examples:

PSA_KEY_TYPE_AES
PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_CURVE_SECP_R1)
PSA_KEY_TYPE_ECC_KEY_PAIR(_)

Driver entry points

Overview of driver entry points

Drivers define functions, each of which implements an aspect of a capability of a driver, such as a cryptographic operation, a part of a cryptographic operation, or a key management action. These functions are called the entry points of the driver. Most driver entry points correspond to a particular function in the PSA Cryptography API. For example, if a call to psa_sign_hash() is dispatched to a driver, it invokes the driver’s sign_hash function.

All driver entry points return a status of type psa_status_t which should use the status codes documented for PSA services in general and for PSA Crypto in particular: PSA_SUCCESS indicates that the function succeeded, and PSA_ERROR_xxx values indicate that an error occurred.

The signature of a driver entry point generally looks like the signature of the PSA Crypto API that it implements, with some modifications. This section gives an overview of modifications that apply to whole classes of entry points. Refer to the reference section for each entry point or entry point family for details.

Some entry points are grouped in families that must be implemented as a whole. If a driver supports a entry point family, it must provide all the entry points in the family.

General considerations on driver entry point parameters

Buffer parameters for driver entry points obey the following conventions:

Buffers of size 0 may be represented with either a null pointer or a non-null pointer.

Input buffers and other input-only parameters (const pointers) may be in read-only memory. Overlap is possible between input buffers, and between an input buffer and an output buffer, but not between two output buffers or between a non-buffer parameter and another parameter.

Driver entry points for single-part cryptographic operations

The following driver entry points perform a cryptographic operation in one shot (single-part operation):

Driver entry points for multi-part operations

General considerations on multi-part operations

The entry points that implement each step of a multi-part operation are grouped into a family. A driver that implements a multi-part operation must define all of the entry points in this family as well as a type that represents the operation context. The lifecycle of a driver operation context is similar to the lifecycle of an API operation context:

  1. The core initializes operation context objects to either all-bits-zero or to logical zero ({0}), at its discretion.
  2. The core calls the xxx_setup entry point for this operation family. If this fails, the core destroys the operation context object without calling any other driver entry point on it.
  3. The core calls other entry points that manipulate the operation context object, respecting the constraints.
  4. If any entry point fails, the core calls the driver’s xxx_abort entry point for this operation family, then destroys the operation context object without calling any other driver entry point on it.
  5. If a “finish” entry point fails, the core destroys the operation context object without calling any other driver entry point on it. The finish entry points are: prefix_mac_sign_finish, prefix_mac_verify_finish, prefix_cipher_fnish, prefix_aead_finish, prefix_aead_verify.

If a driver implements a multi-part operation but not the corresponding single-part operation, the core calls the driver’s multipart operation entry points to perform the single-part operation.

Multi-part operation entry point family "hash_multipart"

This family corresponds to the calculation of a hash in multiple steps.

This family applies to transparent drivers only.

This family requires the following type and functions:

To verify a hash with psa_hash_verify(), the core calls the driver’s *prefix_hash_finish entry point and compares the result with the reference hash value.

For example, a driver with the prefix "acme" that implements the "hash_multipart" entry point family must define the following type and entry points (assuming that the capability does not use the "names" property to declare different type and entry point names):

typedef ... acme_hash_operation_t;
psa_status_t acme_hash_setup(acme_hash_operation_t *operation,
                             psa_algorithm_t alg);
psa_status_t acme_hash_update(acme_hash_operation_t *operation,
                              const uint8_t *input,
                              size_t input_length);
psa_status_t acme_hash_finish(acme_hash_operation_t *operation,
                              uint8_t *hash,
                              size_t hash_size,
                              size_t *hash_length);
psa_status_t acme_hash_abort(acme_hash_operation_t *operation);

Operation family "mac_multipart"

TODO

Operation family "mac_verify_multipart"

TODO

Operation family "cipher_encrypt_multipart"

TODO

Operation family "cipher_decrypt_multipart"

TODO

Operation family "aead_encrypt_multipart"

TODO

Operation family "aead_decrypt_multipart"

TODO

Operation family "key_derivation"

This family requires the following type and entry points:

TODO: key input and output for opaque drivers; deterministic key generation for transparent drivers

TODO

Driver entry points for key management

The driver entry points for key management differs significantly between transparent drivers and opaque drivers. Refer to the applicable section for each driver type.

Miscellaneous driver entry points

Driver initialization

A driver may declare an "init" entry point in a capability with no algorithm, key type or key size. If so, the driver calls this entry point once during the initialization of the PSA Crypto subsystem. If the init entry point of any driver fails, the initialization of the PSA Crypto subsystem fails.

When multiple drivers have an init entry point, the order in which they are called is unspecified. It is also unspecified whether other drivers’ init functions are called if one or more init function fails.

On platforms where the PSA Crypto implementation is a subsystem of a single application, the initialization of the PSA Crypto subsystem takes place during the call to psa_crypto_init(). On platforms where the PSA Crypto implementation is separate from the application or applications, the initialization the initialization of the PSA Crypto subsystem takes place before or during the first time an application calls psa_crypto_init().

The init function does not take any parameter.

Combining multiple drivers

To declare a cryptoprocessor can handle both cleartext and plaintext keys, you need to provide two driver descriptions, one for a transparent driver and one for an opaque driver. You can use the mapping in capabilities’ "names" property to arrange for multiple driver entry points to map to the same C function.

Transparent drivers

Key format for transparent drivers

The format of a key for transparent drivers is the same as in applications. Refer to the documentation of psa_export_key() and psa_export_public_key().

Key management with transparent drivers

Transparent drivers may provide the following key management entry points:

Transparent drivers are not involved when importing, exporting, copying or destroying keys, or when generating or deriving symmetric keys.

Fallback

If a transparent driver entry point is part of a capability which has a true "fallback" property and returns PSA_ERROR_NOT_SUPPORTED, the built-in software implementation will be called instead. Any other value (PSA_SUCCESS or a different error code) is returned to the application.

If there are multiple available transparent drivers, the core tries them in turn until one is declared without a true "fallback" property or returns a status other than PSA_ERROR_NOT_SUPPORTED. The order in which the drivers are called is unspecified and may be different for different entry points.

If a transparent driver entry point is part of a capability where the "fallback" property is false or omitted, the core should not include any other code for this capability, whether built in or in another transparent driver.

Opaque drivers

Opaque drivers allow a PSA Cryptography implementation to delegate cryptographic operations to a separate environment that might not allow exporting key material in cleartext. The opaque driver interface is designed so that the core never inspects the representation of a key. The opaque driver interface is designed to support two subtypes of cryptoprocessors:

Key format for opaque drivers

The format of a key for opaque drivers is an opaque blob. The content of this blob is fully up to the driver. The core merely stores this blob.

Note that since the core stores the key context blob as it is in memory, it must only contain data that is meaningful after a reboot. In particular, it must not contain any pointers or transient handles.

The "key_context" property in the driver description specifies how to calculate the size of the key context as a function of the key type and size. This is an object with the following properties:

The integer properties must be C language constants. A typical value for "base_size" is sizeof(acme_key_context_t) where acme_key_context_t is a type defined in a driver header file.

Size of a dynamically allocated key context

If the core supports dynamic allocation for the key context and chooses to use it, and the driver specification includes the "size_function" property, the size of the key context is at least

size_function(key_type, key_bits)

where size_function is the function named in the "size_function" property, key_type is the key type and key_bits is the key size in bits. The prototype of the size function is

size_t size_function(psa_key_type_t key_type, size_t key_bits);

Size of a statically allocated key context

If the core does not support dynamic allocation for the key context or chooses not to use it, or if the driver specification does not include the "size_function" property, the size of the key context for a key of type key_type and of size key_bits bits is:

Key context size for a secure element with storage

If the key is stored in the secure element and the driver only needs to store a label for the key, use "base_size" as the size of the label plus any other metadata that the driver needs to store, and omit the other properties.

If the key is stored in the secure element, but the secure element does not store the public part of a key pair and cannot recompute it on demand, additionally use the "store_public_key" property with the value true. Note that this only influences the size of the key context: the driver code must copy the public key to the key context and retrieve it on demand in its export_public_key entry point.

Key context size for a secure element without storage

If the key is stored in wrapped form outside the secure element, and the wrapped form of the key plus any metadata has up to N bytes of overhead, use N as the value of the "base_size" property and set the "symmetric_factor" property to 1. Set the "key_pair_size" and "public_key_size" properties appropriately for the largest supported key pair and the largest supported public key respectively.

Key management with opaque drivers

Transparent drivers may provide the following key management entry points:

In addition, secure elements that store the key material internally must provide the following two entry points:

Key creation in a secure element without storage

This section describes the key creation process for secure elements that do not store the key material. The driver must obtain a wrapped form of the key material which the core will store. A driver for such a secure element has no "allocate_key" or "destroy_key" entry point.

When creating a key with an opaque driver which does not have an "allocate_key" or "destroy_key" entry point:

  1. The core allocates memory for the key context.
  2. The core calls the driver’s import, generate, derive or copy function.
  3. The core saves the resulting wrapped key material and any other data that the key context may contain.

To destroy a key, the core simply destroys the wrapped key material, without invoking driver code.

Key management in a secure element with storage

This section describes the key creation and key destruction processes for secure elements that have persistent storage for the key material. A driver for such a secure element has two mandatory entry points:

These functions have the following prototypes:

psa_status_t acme_allocate_key(const psa_key_attributes_t *attributes,
                               uint8_t *key_buffer,
                               size_t key_buffer_size);
psa_status_t acme_destroy_key(const psa_key_attributes_t *attributes,
                              const uint8_t *key_buffer,
                              size_t key_buffer_size);

When creating a persistent key with an opaque driver which has an "allocate_key" entry point:

  1. The core calls the driver’s "allocate_key" entry point. This function typically allocates an internal identifier for the key without modifying the state of the secure element and stores the identifier in the key context. This function should not modify the state of the secure element. It may modify the copy of the persistent state of the driver in memory.

  2. The core saves the key context to persistent storage.

  3. The core calls the driver’s key creation entry point.

  4. The core saves the updated key context to persistent storage.

If a failure occurs after the "allocate_key" step but before the call to the second driver entry point, the core will do one of the following:

To destroy a key, the core calls the driver’s "destroy_key" entry point.

Note that the key allocation and destruction entry point must not rely solely on the key identifier in the key attributes to identify a key. Some implementations of the PSA Crypto API store keys on behalf of multiple clients, and different clients may use the same key identifier to designate different keys. The manner in which the core distinguishes keys that have the same identifier but are part of the key namespace for different clients is implementation-dependent and is not accessible to drivers. Some typical strategies to allocate an internal key identifier are:

TODO: explain constraints on how the driver updates its persistent state for resilience

TODO: some of the above doesn’t apply to volatile keys

Key creation entry points in opaque drivers

The key creation entry points have the following prototypes:

psa_status_t acme_import_key(const psa_key_attributes_t *attributes,
                             const uint8_t *data,
                             size_t data_length,
                             uint8_t *key_buffer,
                             size_t key_buffer_size);
psa_status_t acme_generate_key(const psa_key_attributes_t *attributes,
                               uint8_t *key_buffer,
                               size_t key_buffer_size);

If the driver has an "allocate_key" entry point, the core calls the "allocate_key" entry point with the same attributes on the same key buffer before calling the key creation function.

TODO: derivation, copy

Key export entry points in opaque drivers

The key export entry points have the following prototypes:

psa_status_t acme_export_key(const psa_key_attributes_t *attributes,
                             const uint8_t *key_buffer,
                             size_t key_buffer_size);
                             uint8_t *data,
                             size_t data_size,
                             size_t *data_length);
psa_status_t acme_export_public_key(const psa_key_attributes_t *attributes,
                                    const uint8_t *key_buffer,
                                    size_t key_buffer_size);
                                    uint8_t *data,
                                    size_t data_size,
                                    size_t *data_length);

The core will only call acme_export_public_key on a private key. Drivers implementers may choose to store the public key in the key context buffer or to recalculate it on demand. If the key context includes the public key, it needs to have an adequate size; see “Key format for opaque drivers”.

The core guarantees that the size of the output buffer (data_size) is sufficient to export any key with the given attributes. The driver must set *data_length to the exact size of the exported key.

Opaque driver persistent state

The core maintains persistent state on behalf of an opaque driver. This persistent state consists of a single byte array whose size is given by the "persistent_state_size" property in the driver description.

The core loads the persistent state in memory before it calls the driver’s init entry point. It is adjusted to match the size declared by the driver, in case a driver upgrade changes the size:

The core provides the following callback functions, which an opaque driver may call while it is processing a call from the driver:

psa_status_t psa_crypto_driver_get_persistent_state(uint_8_t **persistent_state_ptr);
psa_status_t psa_crypto_driver_commit_persistent_state(size_t from, size_t length);

psa_crypto_driver_get_persistent_state sets *persistent_state_ptr to a pointer to the first byte of the persistent state. This pointer remains valid during a call to a driver entry point. Once the entry point returns, the pointer is no longer valid. The core guarantees that calls to psa_crypto_driver_get_persistent_state within the same entry point return the same address for the persistent state, but this address may change between calls to an entry point.

psa_crypto_driver_commit_persistent_state updates the persistent state in persistent storage. Only the portion at byte offsets from inclusive to from + length exclusive is guaranteed to be updated; it is unspecified whether changes made to other parts of the state are taken into account. The driver must call this function after updating the persistent state in memory and before returning from the entry point, otherwise it is unspecified whether the persistent state is updated.

The core will not update the persistent state in storage while an entry point is running except when the entry point calls psa_crypto_driver_commit_persistent_state. It may update the persistent state in storage after an entry point returns.

In a multithreaded environment, the driver may only call these two functions from the thread that is executing the entry point.

How to use drivers from an application

Using transparent drivers

Transparent drivers linked into the library are automatically used for the mechanisms that they implement.

Using opaque drivers

Each opaque driver is assigned a location. The driver is invoked for all actions that use a key in that location. A key’s location is indicated by its lifetime. The application chooses the key’s lifetime when it creates the key.

For example, the following snippet creates an AES-GCM key which is only accessible inside a secure element.

psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_lifetime(&attributes, PSA_KEY_LIFETIME_FROM_PERSISTENCE_AND_LOCATION(
        PSA_KEY_PERSISTENCE_DEFAULT, PSA_KEY_LOCATION_acme));
psa_set_key_identifer(&attributes, 42);
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_size(&attributes, 128);
psa_set_key_algorithm(&attributes, PSA_ALG_GCM);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
psa_key_handle_t handle = 0;
psa_generate_key(&attributes, &handle);

Using opaque drivers from an application

Lifetimes and locations

The PSA Cryptography API, version 1.0.0, defines lifetimes as an attribute of a key that indicates where the key is stored and which application and system actions will create and destroy it. The lifetime is expressed as a 32-bit value (typedef uint32_t psa_key_lifetime_t). An upcoming version of the PSA Cryptography API defines more structure for lifetime values to separate these two aspects of the lifetime:

An opaque driver is attached to a specific location. Keys in the default location (PSA_KEY_LOCATION_LOCAL_STORAGE = 0) are transparent: the core has direct access to the key material. For keys in a location that is managed by an opaque driver, only the secure element has access to the key material and can perform operations on the key, while the core only manipulates a wrapped form of the key or an identifier of the key.

Creating a key in a secure element

The core defines a compile-time constant for each opaque driver indicating its location called PSA_KEY_LOCATION_prefix where prefix is the value of the "prefix" property in the driver description. For convenience, Mbed TLS also declares a compile-time constant for the corresponding lifetime with the default persistence called PSA_KEY_LIFETIME_prefix. Therefore, to declare an opaque key in the location with the prefix foo with the default persistence, call psa_set_key_lifetime during the key creation as follows:

psa_set_key_lifetime(&attributes, PSA_KEY_LIFETIME_foo);

To declare a volatile key:

psa_set_key_lifetime(&attributes, PSA_KEY_LIFETIME_FROM_PERSISTENCE_AND_LOCATION(
        PSA_KEY_LOCATION_foo,
        PSA_KEY_PERSISTENCE_VOLATILE));

Generally speaking, to declare a key with a specified persistence:

psa_set_key_lifetime(&attributes, PSA_KEY_LIFETIME_FROM_PERSISTENCE_AND_LOCATION(
        PSA_KEY_LOCATION_foo,
        persistence));

Open questions

Driver declarations

Declaring driver functions

The core may want to provide declarations for the driver functions so that it can compile code using them. At the time of writing this paragraph, the driver headers must define types but there is no obligation for them to declare functions. The core knows what the function names and argument types are, so it can generate prototypes.

It should be ok for driver functions to be function-like macros or function pointers.

Driver location values

How does a driver author decide which location values to use? It should be possible to combine drivers from different sources. Use the same vendor assignment as for PSA services?

Can the driver assembly process generate distinct location values as needed? This can be convenient, but it’s also risky: if you upgrade a device, you need the location values to be the same between builds.

Driver function interfaces

Driver function parameter conventions

Should 0-size buffers be guaranteed to have a non-null pointers?

Should drivers really have to cope with overlap?

Should the core guarantee that the output buffer size has the size indicated by the applicable buffer size macro (which may be an overestimation)?

Partial computations in drivers

Substitution points

Earlier drafts of the driver interface had a concept of substitution points: places in the calculation where a driver may be called. Some hardware doesn’t do the whole calculation, but only the “main” part. This goes both for transparent and opaque drivers. Some common examples:

This concept, or some other way to reuse portable code such as specifying inner functions like psa_rsa_pad in the core, should be added to the specification.

Key management

Mixing drivers in key derivation

How does psa_key_derivation_output_key work when the extraction part and the expansion part use different drivers?

Public key calculation

ECC key pairs are represented as the private key value only. The public key needs to be calculated from that. Both transparent drivers and opaque drivers provide a function to calculate the public key ("export_public_key").

The specification doesn’t mention when the public key might be calculated. The core may calculate it on creation, on demand, or anything in between. Opaque drivers have a choice of storing the public key in the key context or calculating it on demand and can convey whether the core should store the public key with the "store_public_key" property. Is this good enough or should the specification include non-functional requirements?

Opaque drivers

Opaque driver persistent state

The driver is allowed to update the state at any time. Is this ok?

An example use case for updating the persistent state at arbitrary times is to renew a key that is used to encrypt communications between the application processor and the secure element.

psa_crypto_driver_get_persistent_state does not identify the calling driver, so the driver needs to remember which driver it’s calling. This may require a thread-local variable in a multithreaded core. Is this ok?