Service Access Protocols

A trusted service is accessed by calling service-specific methods via an RPC mechanism. The set of callable methods forms the public interface exposed by a service. This section is concerned with interface conventions and protocols used for serializing method parameters and return values. It is anticipated that there will be a need to support different parameter serialization schemes to suite different needs. The project accommodates this with the following:

  • The Protocols directory structure allows for different protocol definitions for the same service.

  • Message serialization code is decoupled from service provider code using an abstract ‘serializer’ interface. Alternative concrete serializers may provide implementations of the interface.

A deployment independent interface for locating services and establishing RPC sessions is described here: Service Locator

Trusted Services protocol layers

../_images/TSProtocolLayers.svg
  • Service client interface: This component provides the interface to a given service for a user application, i.e. the PSA Crypto, Internal Trusted Storage, etc. interface.

  • Service layer: This layer is responsible for serializing and deserializing the service specific parameters and it provides a transparent interface between the caller and endpoint side.

  • RPC layer:

    • RPC caller session: This component provides a session-like object for the service layer. After opening the session, it is tied to the opened service interface of the endpoint. Each service call can use the simple begin/invoke/end interface for requesting a buffer for the call parameters, for invoking the call and for releasing the response buffer.

      The RPC caller session manages the lifetime of the shared memory. Currently it has two options. It either creates the memory on session open and keeps it while the session is open, or it creates the shared buffer for each call and releases it when the call ends (end()).

    • RPC caller implementations (ts_rpc_caller_linux, ts_rpc_caller_sp, etc.): The RPC caller session is built on the primitives of the RPC callers. These primitives allow the caller session finding the remote endpoint, creating and releasing shared memories and doing the actual call.

      The main RPC implementation is the TS RPC which is a TS specific RPC solution over FF-A. The project contains caller implementations for Linux and for S-EL0 SPs. The Linux implementation is split between the user space and a kernel driver.

      There are other RPC caller implementations (dummy, direct) which are used for testing purposes.

    • RPC endpoint (ts_rpc_endpoint_sp): This component provides the RPC endpoint which can host multiple services. Once it receives the call from the client, it finds the matching service and forwards the serialized call parameters to it.

  • FF-A layer: It is the transport layer of the protocol stack, and it provides interfaces for sending messages and sharing memory between normal world and secure world components.

TS RPC implementation

Generic concepts

  • The requests are always sent by the caller and the endpoint sends a response.

  • The protocol version describes the ABI, the allowed values of the message fields and the behavior of the calls.

  • Service endpoints are provided by FF-A secure partitions.

  • Each endpoint can implement multiple services. The services are identified by their service UUID (not FF-A UUID). To avoid including the UUID in each service call, a short interface ID is assigned to each service. The mapping of service UUIDs and interface IDs can be queried by an RPC call. The lifetime of the interface ID is the same as the lifetime of the service. The 0xff interface ID is used for the management interface.

  • The service calls use shared memory to forward the call payload. It has to be shared via FF-A and then retrieved by the endpoint. The shared memories are tied to an endpoint not to a service.

  • The errors which happen in the RPC layer will result in a RPC status code which indicates an error.

  • The errors which happen in the service handler will result in a service status code which indicates an error. In this case the RPC status code will be RPC_SUCCESS as the RPC layer was able to forward the call between the service caller and the service handler.

ABI

The ABI of the TS RPC protocol uses the 32 bit variants of FFA_MSG_SEND_DIRECT_REQ and FFA_MSG_SEND_DIRECT_RESP interfaces of the FF-A specification. The use of the implementation specific arguments is listed in the table below.

TS RPC ABI

Message name

Short message ID

W3[31:30] - SAP

W3[29:24] - Flags

W3[23:16] - Interface ID

W3[15:0] - Opcode

W4 - Arg1

W5 - Arg2

W6 - Arg3

W7 - Arg4

RPC protocol version get request

VERSION_GET

0b00

0b000000

0xff

0x0000

Reserved (MBZ)

Reserved (MBZ)

Reserved (MBZ)

Reserved (MBZ)

RPC protocol version get response

VERSION_GET

0b00

0b000000

0xff

0x0000

Version, starting from 0x00000001

Reserved (MBZ)

Reserved (MBZ)

Reserved (MBZ)

Memory retrieve request

MEM_RETRIEVE

0b00

0b000000

0xff

0x0001

FF-A memory handle LSW

FF-A memory handle MSW

FF-A memory tag LSW

FF-A memory tag MSW

Memory relinquish request

MEM_RELINQUISH

0b00

0b000000

0xff

0x0002

FF-A memory handle LSW

FF-A memory handle MSW

Reserved (MBZ)

Reserved (MBZ)

Memory retrieve/relinquish response

MEM_RETRIEVE/MEM_RELINQUISH

0b00

0b000000

0xff

0x0001/0x0002

TS RPC status

Reserved (MBZ)

Reserved (MBZ)

Reserved (MBZ)

Service info get request

SERVICE_INFO_GET

0b00

0b000000

0xff

0x0003

Service UUID

Service UUID

Service UUID

Service UUID

Service info get response

SERVICE_INFO_GET

0b00

0b000000

0xff

0x0003

TS RPC status

[31:8] Reserved

[7:0] Queried service interface ID

Reserved (MBZ)

Reserved (MBZ)

Service call request

0b00

0b000000

Service interface ID

Service opcode

FF-A memory handle LSW

FF-A memory handle MSW

Request length

Client ID

Service call response

0b00

0b000000

Service interface ID

Service opcode

TS RPC status

Service status

Response length

Reserved

  • RPC protocol version get

    Queries the RPC protocol version. This message must be available and backwards compatible for all protocol versions.

  • Memory retrieve

    Requests the endpoint to do an FFA_MEM_RETRIEVE_REQ call using the forwarded FF-A memory handle and tag.

  • Memory relinquish

    Requests the endpoint to do an FFA_MEM_RELINQUISH call using the forwarded FF-A memory handle.

  • Service info get

    Query service information from the endpoint by the UUID of the service. The UUID is transmitted as defined in SMCCC section 5.3 but in registers W4-W7. The returned service interface ID should be used in the service calls. Multiple endpoints can implement the same service but one endpoint can implement a service only once.

  • Service call

    After creating a shared memory and querying the interface ID for a given service UUID the caller can make a service call. The service opcode and the contents of the shared memory is service specific. The request and response length fields indicate the used area of the shared memory.

    It is allowed to do a limited service call without shared memory, i.e. doorbell call. In this case the FF-A memory ID has to be the invalid handle value 0xffffffffffffffff.

Service discovery

  • Query all TS SPs via FFA_PARTITION_INFO_GET call made to the SPMC. All TS SPs have the same FF-A UUID: bdcd76d7-825e-4751-963b-86d4f84943ac If the system setup has fixed SP endpoint IDs, this step can be skipped.

  • Iterate thought the TS SPs and make a “Service info get request” RPC call to the SPs, containing the service UUID. If the RPC status in the “Service info get response” is RPC_SUCCESS, the SP implements the service and its interface ID is returned in the response.

  • If there are multiple instances of a service, the selection between these should be done in a service specific way (i.e. query service version, capabilities, etc.).

../_images/TSServiceDiscovery.svg

RPC status code values

The status codes for the RPC layer are defined in components/rpc/common/interface/rpc_status.h. Currently the following values are defined:

typedef int32_t rpc_status_t;

#define RPC_SUCCESS			(0)
#define RPC_ERROR_INTERNAL		(-1)
#define RPC_ERROR_INVALID_VALUE		(-2)
#define RPC_ERROR_NOT_FOUND		(-3)
#define RPC_ERROR_INVALID_STATE		(-4)
#define RPC_ERROR_TRANSPORT_LAYER	(-5)
#define RPC_ERROR_INVALID_REQUEST_BODY	(-6)
#define RPC_ERROR_INVALID_RESPONSE_BODY	(-7)
#define RPC_ERROR_RESOURCE_FAILURE	(-8)

Example TS RPC call

This example shows the full sequence of a service call by opening the RPC session, doing the call (begin/invoke/end) and then closing the session. In this case the RPC session it set to create individual shared memory for each call.

'-------------------------------------------------------------------------------
' Copyright (c) 2023, Arm Limited and Contributors. All rights reserved.
'
' SPDX-License-Identifier: BSD-3-Clause
'
'-------------------------------------------------------------------------------

@startuml TS RPC call sequence

autoactivate on

participant "Service caller" as service_caller
participant "RPC caller session" as rpc_session
participant "TS RPC caller" as rpc_caller
participant "SPMC" as spmc
participant "TS RPC endpoint" as endpoint
participant "Service" as service

service_caller -> rpc_session: open(service_uuid)

' Sesssion open
rpc_session -> rpc_caller: find_and_open_session(service_uuid)

rpc_caller -> spmc: FFA_PARTITION_INFO_GET(ts_rpc_uuid)
spmc --> rpc_caller: FF-A endpoint_id

rpc_caller -> endpoint: TS_RPC_VERSION_GET
endpoint --> rpc_caller: TS RPC protocol version

rpc_caller -> endpoint: TS_RPC_SERVICE_INFO_GET(service_uuid)
endpoint --> rpc_caller: Service interface_id

rpc_caller --> rpc_session: TS RPC status

rpc_session --> service_caller: TS RPC status

' Begin
service_caller -> rpc_session: begin(request_length, response_max_length)

rpc_session -> rpc_session: allocate_buffer(MAX(request_length, response_max_length))
rpc_session --> rpc_session: buffer

rpc_session -> rpc_caller: create_shared_memory(endpoint_id, buffer)

rpc_caller -> spmc: FFA_MEM_SHARE(endpoint_id, buffer)
spmc --> rpc_caller: memory_handle, memory_tag

rpc_caller -> endpoint: TS_RPC_MEM_RETRIEVE(memory_handle, memory_tag)

endpoint -> spmc: FFA_MEM_RETRIEVE(memory_handle, memory_tag)
spmc --> endpoint: buffer

endpoint --> rpc_caller: TS RPC status

rpc_caller --> rpc_session: TS RPC status, shared_memory

rpc_session --> service_caller: Request buffer

' Serialize
service_caller -> service_caller: Serialize parameters to request buffer
service_caller --> service_caller

' Invoke
service_caller -> rpc_session: invoke(opcode)

rpc_session -> rpc_caller: call(opcode, shared_memory, request_length)

rpc_caller -> endpoint: service_call(interface_id, opcode, memory_handle, request_length)

endpoint -> service: service_call(opcode, request)
service --> endpoint: service status, response

endpoint --> rpc_caller: TS RPC status, service status, response length

rpc_caller --> rpc_session: TS RPC status, service status, response length

rpc_session --> service_caller: TS RPC status, service status, response

' Deserialize
service_caller -> service_caller: Deserialize return values from the response buffer
service_caller --> service_caller

' End
service_caller -> rpc_session: end()

rpc_session -> rpc_caller: release_shared_memory(shared_memory)

rpc_caller -> endpoint: TS_RPC_MEM_RELINQUISH(memory_handle)

endpoint -> spmc: FFA_MEMORY_RELINQUISH(memory_handle)
spmc --> endpoint: FFA_SUCCESS

endpoint --> rpc_caller: TS RPC status

rpc_caller -> spmc: FFA_MEM_RECLAIM(memory_handle)
spmc --> rpc_caller: FFA_SUCCESS

rpc_caller -> rpc_caller: free(buffer)
rpc_caller --> rpc_caller

rpc_caller --> rpc_session: TS RPC status

rpc_session --> service_caller: TS RPC status

service_caller -> rpc_session: close()
rpc_session --> service_caller: TS RPC status
@enduml

Note

Although the TS RPC layer messages use FFA_MSG_SEND_DIRECT_REQ/FFA_MSG_SEND_DIRECT_RESP interface and go through the SPMC their destination is not the SPMC but the RPC endpoint. For simplifying the diagram, these calls are showed as direct calls between the TS RPC caller and the TS RPC endpoint.

Status Codes

On returning from a request to invoke a service method, two status codes are returned:

  • RPC status - A generic status code that corresponds to the RPC call transaction. RPC status codes are standardized across all services. (See: RPC status code values)

  • Service status - a service specific status code. (See: Service Status Codes )

Separation of status codes by layer allows service specific status codes to be accommodated while keeping RPC status codes common.

A client should only check the returned service status if the returned RPC status value is RPC_SUCCESS. All other RPC status values indicate that an error occurred in delivering the RPC request. An RPC status of RPC_SUCCESS does not indicate that the service was successful. It merely indicates that the request was delivered, a suitable handler was identified and the request parameters were understood.

Service Access Protocol Definition Conventions

A service access protocol defines the following:

  • Opcodes used for identifying service methods.

  • Request parameters for each method.

  • Response parameters for method return values.

  • Operation status code.

Details of how public interface definition files for trusted services are organized, see: Project Structure

It is possible that for certain deployments, it will be necessary to customize which parameter encoding scheme is used. Many schemes are possible such as Protocol Buffers, CBOR, JSON, TLV, TPM commands or packed C structures. To make scheme customization straight forward, serialize/deserialize operations should be encapsulated behind a common interface to decouple service provider code from any particular serialization scheme. A section below describes a pattern for achieving this.

Service Namespace

Definitions that form a service access protocol should live within a namespace that is unique for the particular service. Using a namespace for service definitions avoids possible clashes between similarly named definitions that belong to different services. How the namespace is implemented depends on how the access protocol is defined. For example, the Protocol Buffers definitions for the crypto service all live within the ts_crypto package. The recommended convention for forming a trusted service namespace is as follows:

ts_<service_name>

e.g.
ts_crypto
ts_secure_storage

Language Independent Protocol Definitions

By defining service access protocols using an interface description language (IDL) with good support for different programming languages, it should be straight forward to access trusted services from clients written in a range of languages. On Arm Cortex-A deployments, it is common for user applications to be implemented using a range of languages such as Go, Python or Java. Rather than relying on a binding to a C client library, native client code may be generated from the formal protocol definition files. Initial protocol definitions use Google Protocol Buffers as the IDL. The project structure allows for use of alternative definition schemes and serializations.

Opcode Definition

Opcodes are integer values that identify methods implemented by a service endpoint. Opcodes only need to be unique within the scope of a particular service. The mapping of opcode to method is an important part of a service interface definition and should be readily available to clients written in a variety of programming languages. For a Protocol Buffers based definition, opcodes are defined in a file called:

opcodes.proto

For example, for the Crypto trusted service, the Protocol Buffers opcode definitions are in:

protocols/service/crypto/protobuf/opcodes.proto

Alternative definitions for light-weight C clients using the packed-c scheme are in:

protocols/service/crypto/packed-c/opcodes.h

Parameter Definition

The convention used for serializing method parameters and return values may be specific to a particular service. The definition file will include message definitions for both request and response parameters. Common objects that are used for multiple methods should be defined in separate files. When using Protobufs, the following naming convention for method parameter files should be used:

<method_name>.proto

For example, the Crypto export_public_key method is defined in a file called:

protocols/service/crypto/protobuf/export_public_key.proto

Service Status Codes

Service specific status code definitions using different definition schemes are defined here (using crypto service as an example):

protocols/service/crypto/protobuf/status.proto
protocols/service/crypto/packed-c/status.h

Status code definitions may also be shared between services. For example, services that conform to PSA API conventions will use standardized PSA status codes, defined here:

protocols/service/psa/protobuf/status.proto
protocols/service/psa/packed-c/status.h

Use of Protocol Buffers

When Protocol Buffers is used for protocol definition and parameter serialization, the following conventions have been adopted.

.proto File Style Guide

The style of the .proto files should follow Google’s Protocol Buffers Style Guide.

Protocol Buffer Library for Trusted Services

Protocol Buffers standardizes how service interfaces are defined and the on-wire encoding for messages. Because of this, service clients and service providers are free to use any conformant implementation. However for trusted services that may be deployed across a range of environments, some of which may be resource constrained, a lightweight library should be used for C/C++ code that implement or use trusted services. For this purpose, Nanobp (https://github.com/nanopb/nanopb) should be used.

Serialization Protocol Flexibility

Many different serialization protocols exist for encoding and decoding message parameters. Hard-wiring a particular protocol into a trusted service provider implementation isn’t desirable for the following reasons:

  • Depending on the complexity of serialization operations, mixing serialization logic with protocol-independent code makes trusted service provider code bigger and more difficult to maintain.

  • Different protocols may be needed for different deployments. It should be possible to make a build-time or even a run-time selection of which protocol to use.

  • The number of supported serializations protocols is likely to grow. Adding a new protocol shouldn’t require you to make extensive code changes and definitely shouldn’t break support for existing protocols.

These problems can be avoided by implementing protocol specific operations behind a common interface. Serialize/deserialize operations will have the following pattern:

int serialize_for_method(msg_buffer *buf, in args...);
int deserialize_for_method(const msg_buffer *buf, out args...);

Encoding types are represented as dedicated service interfaces in the RPC protocol and as such are identified by a uniq service UUID.


Copyright (c) 2020-2023, Arm Limited and Contributors. All rights reserved.

SPDX-License-Identifier: BSD-3-Clause