Arm’s Platform Security Architecture (PSA) was released in 2017, but it was two years until the first beta release of the PSA Cryptography API in 2019, and another year until the 1.0 specification in 2020. Aimed at securing connected devices and originally targeting only Arm-based systems, PSA has evolved with the donation of the PSA Certified program to GlobalPlatform in 2025, allowing non-Arm devices, such as popular RISC-V microcontrollers (MCUs), to achieve certification. Arm still provides resources to assist in the certification process, such as developing the specifications for PSA Certified APIs, which include the Crypto API, Secure Storage API, Attestation API, Firmware Update API, and Status Code API.

The distinction between resources targeting Arm platforms and those that are generally applicable can be confusing, but for the most part, the PSA Certfied APIs are platform-agnostic, while Arm provides additional resources that extend beyond specification and into implementation of the APIs using their hardware IP. For example, Trusted Firmware M (TF-M) is a reference implementation of PSA, which relies on the boundary between the Secure Processing Environment (SPE) and Non-Secure Processing Environment (NSPE) on Arm Cortex-M platforms. Armv8-M provides this hardware-level isolation via the Cortex-M Security Extension (CMSE), more commonly referred to as TrustZone.

I have previously written about Arm Cortex-M hardware security states here.

The PSA APIs create a consistent programming interface for which many implementations may exist. At my day job at Golioth (now part of Canonical), we have been tracking the adoption of the PSA Crypto API over the past few years. Because we typically develop embedded software that enables connectivity and targets a wide variety of hardware and platforms, we are intimately familiar with the security capabilites of different processor architectures, microcontrollers, and RTOS’s. However, ensuring that we are taking advantage of not just the security features, but also the available cryptographic acceleration, has been painful. On highly constrained devices, performance is an integral component of security. Slow or power hungry cryptographic operations can be enough to sink a product that needs to operate for an extended period of time on a small battery. In the worst case scenario, product makers punt on security because of the complexity it introduces into the system.

MbedTLS v4.0.0 and TF-PSA-Crypto Link to heading

If you work on connected embedded systems, you have almost certainly either been migrating your own firmware to leverage PSA APIs, or been watching closely as libraries you depend on, such as MbedTLS have been rearchitected to target PSA APIs or offer implementations. In October of 2025, the v4.0.0 release officially split out the PSA Crypto API implementation of MbedTLS into TF-PSA-Crypto.

MbedTLS, a library that traditionally has offered TLS / DTLS support, X.509 certificate utilities, and various cryptographic primitives, has been incorporating the PSA Crypto API since the v2.15.0 release in 2018. The recent step of splitting out the PSA Crypto implementation imposes non-trivial migration work on consumers of MbedTLS, but it also takes an important step towards enabling them to standardize on the API.

While MbedTLS has supported alternative drivers in the past to allow for hardware acceleration and proprietary cryptography libaries, the new TF-PSA-Crypto library offers a much cleaner mechanism, where both transparent and opaque drivers can be implemented. Transparent drivers require the passing of plaintext keys, making them suitable for pure software and hardware accelerated cryptographic operations, while opaque drivers operate on keys stored in secure locations, such as a physically separate secure element or an integrated secure enclave. Driver capabilities can be declared using a JSON syntax, then the necessary wrappers can be auto-generated to call into the driver implemenations. For example, the test opaque driver is specified in the following JSON file.

{
    "prefix":       "mbedtls_test",
    "type":         "opaque",
    "location":     "0x7fffff",
    "mbedtls/h_condition":   "defined(PSA_CRYPTO_DRIVER_TEST)",
    "headers":      ["test/drivers/test_driver.h"],
    "capabilities": [
        {
            "_comment":     "The Mbed TLS opaque driver supports import key/export key/export_public key",
            "mbedtls/c_condition":    "defined(PSA_CRYPTO_DRIVER_TEST)",
            "entry_points": ["import_key", "export_key", "export_public_key"]
        },
        {
            "_comment":     "The Mbed TLS opaque driver supports copy key/ get builtin key",
            "mbedtls/c_condition":    "defined(PSA_CRYPTO_DRIVER_TEST)",
            "entry_points": ["copy_key", "get_builtin_key"],
            "names":         {"copy_key":"mbedtls_test_opaque_copy_key", "get_builtin_key":"mbedtls_test_opaque_get_builtin_key"}
        }
     ]
}

There are template header files that use Jinja to automatically incorporate the driver entry points in the wrapper calls.

static inline psa_status_t psa_driver_wrapper_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,
    size_t *key_buffer_length,
    size_t *bits )
{
{% with entry_point = "import_key" -%}
{% macro entry_point_param(driver) -%}
attributes,
data,
data_length,
key_buffer,
key_buffer_size,
key_buffer_length,
bits
{% endmacro %}
    psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED;
    psa_key_location_t location = PSA_KEY_LIFETIME_GET_LOCATION(
                                      psa_get_key_lifetime( attributes ) );

    switch( location )
    {
        case PSA_KEY_LOCATION_LOCAL_STORAGE:
            /* Key is stored in the slot in export representation, so
             * cycle through all known transparent accelerators */
#if defined(PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT)
{% with nest_indent=12 %}
{% include "OS-template-transparent.jinja" -%}
{% endwith -%}
#endif /* PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT */

            /* Fell through, meaning no accelerator supports this operation */
            return( psa_import_key_into_slot( attributes,
                                              data, data_length,
                                              key_buffer, key_buffer_size,
                                              key_buffer_length, bits ) );
        /* Add cases for opaque driver here */
#if defined(PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT)
{% with nest_indent=8 %}
{% include "OS-template-opaque.jinja" -%}
{% endwith -%}
#endif /* PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT */
        default:
            (void)status;
            return( PSA_ERROR_INVALID_ARGUMENT );
    }
{% endwith %}
}

While there are currently limited entry points supported by auto-generation, additional entry points can be enabled through a more manual procedure. The primary benefit of this architecture is that platform developers and silicon vendors can easily introduce new drivers, and conditionally enable them based on the hardware the consumer is targeting. Meanwhile, library authors, and consumers themselves, can leverage the same APIs across a wide range of systems. In fact, while the TF-PSA-Crypto library makes the modular inclusion of cryptographic implementations simpler, vendors can also opt to implement the entire PSA Crypto API themselves without breaking compatibility.

Portability in Practice Link to heading

In one of our newer embedded libraries, signy, which generates signed URLs on embedded devices and thus must interact with private keys and perform cryptographic operations, we made the decision to build solely on the PSA Crypto API from the beginning. This made expanding support from just Zephyr to Espressif’s FreeRTOS-based esp-idf straightforward, despite implementations of cryptographic operations looking quite different.

For example, to generate the signature portion of the signed URL, signy calls psa_sign_hash().

    /* Sign the hash */
    status = psa_sign_hash(signy_key,
                           PSA_ALG_ECDSA(PSA_ALG_SHA_256),
                           hash,
                           sizeof(hash),
                           sig,
                           sizeof(sig),
                           &sig_len);
    if (status != PSA_SUCCESS)
    {
        err = -EINVAL;
        goto cleanup;
    }

When building for the nRF52840 development kit from Nordic Semiconductor, we build on top of the nRF Connect SDK (NCS), a downstream fork of Zephyr. The nRF52840 includes an Arm Cortex-M4 core, which does not support hardware-level isolation via the aforementioned Cortex-M Security Extension (CMSE). However, it does include the Arm TrustZone CryptoCell 310 subsystem, which provides root of trust and security services, including a Public Key Accelerator (PKA) engine.

NCS allows us to leverage this capability through its (somewhat complex) cryptographic architecture. It uses the sdk-oberon-psa-crypto library rather than MbedTLS or TF-PSA-Crypto as the PSA Crypto API implementation, which allows for configuration of multiple drivers. In the case of the nRF52840, we enable the nrf_cc3xx driver, making the cc3xx_sign_hash() function available (this eventually calls into a closed source library with cc3xx_internal_ecdsa_sign()).

/** \defgroup psa_asym_sign PSA driver entry points for asymmetric sign/verify
 *
 *  Entry points for asymmetric message signing and signature verification as
 *  described by the PSA Cryptoprocessor Driver interface specification
 *
 *  @{
 */
psa_status_t cc3xx_sign_hash(const psa_key_attributes_t *attributes, const uint8_t *key,
			     size_t key_length, psa_algorithm_t alg, const uint8_t *input,
			     size_t input_length, uint8_t *signature, size_t signature_size,
			     size_t *signature_length)
{
	if (alg != PSA_ALG_RSA_PKCS1V15_SIGN_RAW &&
	    input_length != PSA_HASH_LENGTH(PSA_ALG_SIGN_GET_HASH(alg))) {
		return PSA_ERROR_INVALID_ARGUMENT;
	}

	if (!PSA_KEY_TYPE_IS_ASYMMETRIC(psa_get_key_type(attributes))) {
		return PSA_ERROR_NOT_SUPPORTED;
	}

	if (PSA_ALG_IS_ECDSA(alg)) {
		return cc3xx_internal_ecdsa_sign(attributes, key, key_length, alg, input,
						 input_length, signature, signature_size,
						 signature_length, false);
	} else if (PSA_ALG_IS_RSA_PKCS1V15_SIGN(alg) || PSA_ALG_IS_RSA_PSS(alg)) {
		return cc3xx_internal_rsa_sign(attributes, key, key_length, alg, input,
					       input_length, signature, signature_size,
					       signature_length, false);
	}

	return PSA_ERROR_NOT_SUPPORTED;
}

The Oberon library calls into the nrf_security subsystem, which looks very similar to TF-PSA-Crypto, dispatching PSA Crypto API operations to the enabled driver(s).

psa_status_t psa_driver_wrapper_sign_hash(const psa_key_attributes_t *attributes,
					  const uint8_t *key_buffer, size_t key_buffer_size,
					  psa_algorithm_t alg, const uint8_t *hash,
					  size_t hash_length, uint8_t *signature,
					  size_t signature_size, size_t *signature_length)
{
	psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED;
	psa_key_location_t location = PSA_KEY_LIFETIME_GET_LOCATION(attributes->lifetime);

	switch (location) {
	case PSA_KEY_LOCATION_LOCAL_STORAGE:
#if defined(PSA_CRYPTO_DRIVER_TFM_BUILTIN_KEY_LOADER)
	case TFM_BUILTIN_KEY_LOADER_KEY_LOCATION:
#endif		/* defined(PSA_CRYPTO_DRIVER_TFM_BUILTIN_KEY_LOADER) */
		/* Key is stored in the slot in export representation, so
		 * cycle through all known transparent accelerators
		 */

#if defined(PSA_NEED_CRACEN_ASYMMETRIC_SIGNATURE_DRIVER)
	case PSA_KEY_LOCATION_CRACEN:
#if defined(PSA_NEED_CRACEN_KMU_DRIVER)
	case PSA_KEY_LOCATION_CRACEN_KMU:
#endif /* PSA_NEED_CRACEN_KMU_DRIVER */
		status = cracen_sign_hash(attributes, key_buffer, key_buffer_size, alg, hash,
					  hash_length, signature, signature_size, signature_length);
		/* Declared with fallback == true */
		if (status != PSA_ERROR_NOT_SUPPORTED) {
			return status;
		}
#endif /* PSA_NEED_CRACEN_ASYMMETRIC_SIGNATURE_DRIVER */
#if defined(PSA_NEED_CC3XX_ASYMMETRIC_SIGNATURE_DRIVER)
		status = cc3xx_sign_hash(attributes, key_buffer, key_buffer_size, alg, hash,
					 hash_length, signature, signature_size, signature_length);
		/* Declared with fallback == true */
		if (status != PSA_ERROR_NOT_SUPPORTED) {
			return status;
		}
#endif /* PSA_NEED_CC3XX_ASYMMETRIC_SIGNATURE_DRIVER */
#if defined(PSA_NEED_OBERON_ASYMMETRIC_SIGNATURE_DRIVER)
		status = oberon_sign_hash(attributes, key_buffer, key_buffer_size, alg, hash,
					  hash_length, signature, signature_size, signature_length);
		/* Declared with fallback == true */
		if (status != PSA_ERROR_NOT_SUPPORTED) {
			return status;
		}
#endif /* PSA_NEED_OBERON_ASYMMETRIC_SIGNATURE_DRIVER */
		/* Fell through, meaning nothing supports this operation */
		(void)attributes;
		(void)key_buffer;
		(void)key_buffer_size;
		(void)alg;
		(void)hash;
		(void)hash_length;
		(void)signature;
		(void)signature_size;
		(void)signature_length;
		return PSA_ERROR_NOT_SUPPORTED;
		/* Add cases for opaque driver here */
	default:
		/* Key is declared with a lifetime not known to us */
		(void)status;
		return PSA_ERROR_INVALID_ARGUMENT;
	}
}

When building for Espressif devices, we target esp-idf. Espressif maintains a fork of MbedTLS, where changes can be implemented to support specific capabilities of ESP32 devices. In theory, once the auto-generation functionality in TF-PSA-Crypto is expanded to cover all entrypoints, there would no longer be a need to maintain a fork. For the time being, commits such as this one are necessary to modify wrappers to call into specific esp-idf functions.

Even when manual changes are required, the Espressif MbedTLS maintainers have been adding driver JSON files. For example, the hardware accelerated ECDSA opaque driver is specified as follows.

{
    "prefix":       "esp_ecdsa",
    "type":         "opaque",
    "mbedtls/h_condition":   "defined(ESP_ECDSA_DRIVER_ENABLED), defined(ESP_ECDSA_SIGN_DRIVER_ENABLED)",
    "headers":      ["../../../port/psa_driver/include/psa_crypto_driver_esp_ecdsa.h"],
    "capabilities": [
        {
            "mbedtls/c_condition": "defined(ESP_ECDSA_DRIVER_ENABLED), defined(ESP_ECDSA_SIGN_DRIVER_ENABLED)",
            "_comment": "This driver will support ECDSA operations using the ESP hardware accelerator.",
            "entry_points": ["sign_hash", "export_public_key", "import_key"],
            "algorithms": ["PSA_ALG_ECDSA(PSA_ALG_SHA_256), PSA_ALG_ECDSA(PSA_ALG_SHA_384)"],
            "key_types": ["PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)"],
            "key_sizes": [192, 256, 384],
            "fallback": false
        }
    ]
}

In the modified psa_driver_wrapper_sign_hash() implementation, there are a variety of transparent and opaque drivers that may be selected. For devices that support ECDSA acceleration, the call to psa_sign_hash() in signy will result in a call to esp_ecdsa_opaque_sign_hash().

static inline psa_status_t psa_driver_wrapper_sign_hash(
    const psa_key_attributes_t *attributes,
    const uint8_t *key_buffer, size_t key_buffer_size,
    psa_algorithm_t alg, const uint8_t *hash, size_t hash_length,
    uint8_t *signature, size_t signature_size, size_t *signature_length )
{
    psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED;
    psa_key_location_t location =
        PSA_KEY_LIFETIME_GET_LOCATION( psa_get_key_lifetime(attributes) );

    switch( location )
    {
        case PSA_KEY_LOCATION_LOCAL_STORAGE:
            /* Key is stored in the slot in export representation, so
             * cycle through all known transparent accelerators */
#if defined(PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT)
#if defined(PSA_CRYPTO_DRIVER_TEST)
            status = mbedtls_test_transparent_signature_sign_hash( attributes,
                                                           key_buffer,
                                                           key_buffer_size,
                                                           alg,
                                                           hash,
                                                           hash_length,
                                                           signature,
                                                           signature_size,
                                                           signature_length );
            /* Declared with fallback == true */
            if( status != PSA_ERROR_NOT_SUPPORTED )
                return( status );
#endif /* PSA_CRYPTO_DRIVER_TEST */
#if defined (MBEDTLS_PSA_P256M_DRIVER_ENABLED)
            if( PSA_KEY_TYPE_IS_ECC( psa_get_key_type(attributes) ) &&
                PSA_ALG_IS_RANDOMIZED_ECDSA(alg) &&
                PSA_KEY_TYPE_ECC_GET_FAMILY(psa_get_key_type(attributes)) == PSA_ECC_FAMILY_SECP_R1 &&
                psa_get_key_bits(attributes) == 256 )
            {
                status = p256_transparent_sign_hash( attributes,
                                                     key_buffer,
                                                     key_buffer_size,
                                                     alg,
                                                     hash,
                                                     hash_length,
                                                     signature,
                                                     signature_size,
                                                     signature_length );
                if( status != PSA_ERROR_NOT_SUPPORTED )
                return( status );
            }
#endif /* MBEDTLS_PSA_P256M_DRIVER_ENABLED */
#endif /* PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT */
            /* Fell through, meaning no accelerator supports this operation */
            return( psa_sign_hash_builtin( attributes,
                                           key_buffer,
                                           key_buffer_size,
                                           alg,
                                           hash,
                                           hash_length,
                                           signature,
                                           signature_size,
                                           signature_length ) );

        /* Add cases for opaque driver here */
#if defined(PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT)
#if defined(PSA_CRYPTO_DRIVER_TEST)
        case PSA_CRYPTO_TEST_DRIVER_LOCATION:
            return( mbedtls_test_opaque_signature_sign_hash( attributes,
                                                             key_buffer,
                                                             key_buffer_size,
                                                             alg,
                                                             hash,
                                                             hash_length,
                                                             signature,
                                                             signature_size,
                                                             signature_length ) );
#endif /* PSA_CRYPTO_DRIVER_TEST */
#if defined(ESP_ECDSA_DRIVER_ENABLED) && defined(ESP_ECDSA_SIGN_DRIVER_ENABLED)
        case PSA_KEY_LOCATION_ESP_ECDSA:
            if( PSA_KEY_TYPE_IS_ECC( psa_get_key_type(attributes) ) &&
                PSA_ALG_IS_ECDSA(alg) && PSA_ALG_IS_ECDSA( psa_get_key_algorithm(attributes) ) &&
                PSA_KEY_TYPE_ECC_GET_FAMILY(psa_get_key_type(attributes)) == PSA_ECC_FAMILY_SECP_R1)
            {
                return ( esp_ecdsa_opaque_sign_hash( attributes,
                                                key_buffer,
                                                key_buffer_size,
                                                alg,
                                                hash,
                                                hash_length,
                                                signature,
                                                signature_size,
                                                signature_length ) );
            }
            return PSA_ERROR_INVALID_ARGUMENT;
#endif /* defined(ESP_ECDSA_DRIVER_ENABLED) && defined(ESP_ECDSA_SIGN_DRIVER_ENABLED) */
#if defined(ESP_RSA_DS_DRIVER_ENABLED)
        case PSA_KEY_LOCATION_ESP_RSA_DS:
            if( PSA_KEY_TYPE_IS_RSA( psa_get_key_type(attributes) )  &&
                (PSA_ALG_IS_RSA_PKCS1V15_SIGN(alg) || PSA_ALG_IS_RSA_PSS(alg)))
            {
                return( esp_rsa_ds_opaque_signature_sign_hash( attributes,
                                                        key_buffer,
                                                        key_buffer_size,
                                                        alg,
                                                        hash,
                                                        hash_length,
                                                        signature,
                                                        signature_size,
                                                        signature_length ) );
            }
            return PSA_ERROR_INVALID_ARGUMENT;
#endif /* ESP_RSA_DS_DRIVER_ENABLED */
#if defined(SECURE_ELEMENT_DRIVER_ENABLED)
        case PSA_KEY_LOCATION_SECURE_ELEMENT:
            return( secure_element_opaque_sign_hash(
                        attributes,
                        key_buffer, key_buffer_size,
                        alg, hash, hash_length,
                        signature, signature_size,
                        signature_length ) );
#endif /* SECURE_ELEMENT_DRIVER_ENABLED */
#endif /* PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT */
        default:
            /* Key is declared with a lifetime not known to us */
            (void)status;
            return( PSA_ERROR_INVALID_ARGUMENT );
    }
}

The implementation of the esp_ecdsa PSA driver leverages the esp_hal_security component’s ecdsa_hal functionality to interact with the hardware accelerator directly.

void ecdsa_hal_gen_signature(ecdsa_hal_config_t *conf, const uint8_t *hash,
                             uint8_t *r_out, uint8_t *s_out, uint16_t len)
{
    if (len != ECDSA_HAL_P192_COMPONENT_LEN && len != ECDSA_HAL_P256_COMPONENT_LEN
#if SOC_ECDSA_SUPPORT_CURVE_P384
            && len != ECDSA_HAL_P384_COMPONENT_LEN
#endif /* SOC_ECDSA_SUPPORT_CURVE_P384 */
       ) {
        HAL_ASSERT(false && "Incorrect length");
    }

    if (conf->sha_mode == ECDSA_Z_USER_PROVIDED && hash == NULL) {
        HAL_ASSERT(false && "Mismatch in SHA configuration");
    }

    if (ecdsa_ll_get_state() != ECDSA_STATE_IDLE) {
        HAL_ASSERT(false && "Incorrect ECDSA state");
    }

    configure_ecdsa_periph(conf);

#if HAL_CONFIG(ECDSA_GEN_SIG_CM)
#if SOC_IS(ESP32H2)
    if (!ESP_CHIP_REV_ABOVE(efuse_hal_chip_revision(), 102)) {
        ecdsa_hal_gen_signature_with_countermeasure(hash, r_out, s_out, len);
        return;
    }
#endif
    ecdsa_hal_gen_signature_with_countermeasure(hash, r_out, s_out, len);
#else /* HAL_CONFIG_ECDSA_GEN_SIG_CM */
    ecdsa_hal_gen_signature_inner(hash, r_out, s_out, len);
#endif /* !HAL_CONFIG_ECDSA_GEN_SIG_CM */

}

We’ll talk about the countermeasure approach here and power analysis attacks in general in a future post.

This looks slightly different depending on the specific silicon, but for the most part the low level (ll) layer is copying data from buffers into various registers. For example, setting the various parameters for an ECDSA signature operations looks as follows on the ESP32-P4.

/**
 * @brief Write the ECDSA parameter
 *
 * @param param Parameter to be written
 * @param buf   Buffer containing data
 * @param len   Length of buffer
 */
static inline void ecdsa_ll_write_param(ecdsa_ll_param_t param, const uint8_t *buf, uint16_t len)
{
    uint32_t reg;
    uint32_t word;
    switch (param) {
    case ECDSA_PARAM_R:
        reg = ECDSA_R_MEM;
        break;
    case ECDSA_PARAM_S:
        reg = ECDSA_S_MEM;
        break;
    case ECDSA_PARAM_Z:
        reg = ECDSA_Z_MEM;
        break;
    case ECDSA_PARAM_QAX:
        reg = ECDSA_QAX_MEM;
        break;
    case ECDSA_PARAM_QAY:
        reg = ECDSA_QAY_MEM;
        break;
    default:
        HAL_ASSERT(false && "Invalid parameter");
        return;
    }

    for (int i = 0; i < len; i += 4) {
        memcpy(&word, buf + i, 4);
        REG_WRITE(reg + i, word);
    }
}

What Comes Next? Link to heading

We have seen that the PSA Crypto API allows signy to leverage different software libraries and hardware capabilities across Arm, Xtensa, and RISC-V architectures with relatively minimal effort. This is just one example of how library authors are able to support more hardware faster, without compromising on performance or security.

This adoption may also make migration to new algorithms simpler. If you’ve been paying attention to the news, you may have seen that recent publications have caused some individauls and organizations to justifiably accelerate their roadmap towards post-quantum cryptography (PQC) adoption. Just recently, TF-PSA-Crypto added the beginnings of a PQC driver in-tree, based on the Post-Quantum Cryptography Alliance’s (PQCA) mldsa-native library.

Similarly, in January of this year, Arm released a final 1.4 version of the PSA Certified Crypto API 1.4 PQC Extension, which extends the PSA Crypto API with NIST approved PQC schemes. While there are certainly other complications for constrained devices that will come with migration, namely impacts on resource usage and performance, the specification and broad adoption of PSA has dramatically improved the starting position for the ecosystem.