Skip to main content

<alp/security.h> — MbedTLS PSA Crypto

Cryptographic primitives backed by MbedTLS PSA Crypto on Zephyr / bare-metal, and OpenSSL on Yocto. The application API is identical across backends.

#include <alp/security.h>

Hashing

Both hash and AEAD contexts are opaque handles from an _open() call — not caller-allocated structs.

alp_hash_t *h = alp_hash_open(ALP_HASH_SHA256);
if (h == NULL) {
return alp_last_error();
}
alp_hash_update(h, (const uint8_t *)"hello", 5);

uint8_t digest[32];
size_t digest_len = 0;
alp_status_t rc = alp_hash_finish(h, digest, sizeof(digest), &digest_len);
// on ALP_OK, h is already closed — do not close it again
AlgorithmDigest length
ALP_HASH_SHA25632 B
ALP_HASH_SHA38448 B
ALP_HASH_SHA51264 B

alp_hash_finish handle lifecycle

Only ALP_OK implicitly closes the handle. This contract was tightened in v0.10 (GHSA-92c3-v48m-m5gg) — the previous "implicitly closes on success" wording left the short-buffer case ambiguous, and callers could double-close or leak.

ReturnHandle stateWhat to do
ALP_OKClosed for youDo not call alp_hash_close — the digest is written.
ALP_ERR_INVAL (digest_cap too small)Left open and unchangeddigest_len receives the required length. Call again with a large-enough buffer, or alp_hash_close explicitly.
Any other failureLeft safely closeableAlways follow up with alp_hash_close (a redundant close is a harmless no-op).

The short-buffer case is therefore a retryable size query, not a fatal error — the handle survives, so you can size the buffer and call again:

uint8_t small[16]; // too small for SHA-256's 32 B
size_t need = 0;

if (alp_hash_finish(h, small, sizeof(small), &need) == ALP_ERR_INVAL) {
uint8_t *buf = malloc(need); // h is still open; need == 32
alp_hash_finish(h, buf, need, &need);
}

:::note A NULL buffer is not a size query alp_hash_finish rejects digest_out == NULL or digest_cap == 0 with ALP_ERR_INVAL up front, without ever populating digest_len. To probe the length you must pass a real (if undersized) buffer — or just size it from the algorithm's digest length in the table above, which is fixed and known. :::

AEAD (authenticated encryption)

alp_aead_t *a = alp_aead_open(ALP_AEAD_AES_128_GCM, key, sizeof(key));

uint8_t ciphertext[64];
uint8_t tag[16];

alp_aead_encrypt(a,
iv, sizeof(iv),
aad, sizeof(aad),
plaintext, plaintext_len,
ciphertext,
tag, sizeof(tag));

uint8_t recovered[64];
alp_status_t rc = alp_aead_decrypt(a,
iv, sizeof(iv),
aad, sizeof(aad),
ciphertext, plaintext_len,
tag, sizeof(tag),
recovered);
// rc == ALP_ERR_IO on tag mismatch — recovered MUST be discarded

alp_aead_close(a); // wipes key material

Ciphertext is the same size as the plaintext, so there is no separate output-length parameter.

Supported ciphers: ALP_AEAD_AES_128_GCM, ALP_AEAD_AES_256_GCM, ALP_AEAD_CHACHA20_POLY1305.

:::caution tag_len must be exactly 16 v0.10 tightened this: tag_len must be exactly 16 B — the only length every backend round-trips for the algorithms above. Any other value is rejected with ALP_ERR_INVAL before any crypto runs. (The previous contract said "must be ≥ 16", which admitted lengths some backends silently truncated.) :::

:::warning Plaintext is transiently unauthenticated during alp_aead_decrypt During the call, plain_out transiently holds plaintext that has not yet been verified against tag — the backend streams it in before the tag check completes. Treat any bytes observed in that buffer before the function returns as unauthenticated.

This matters for AMP shared-memory buffers another core could poll: decrypt into core-private memory, and only publish after ALP_OK.

The wipe is not unconditional (clarified in v0.11.0): once the backend has been invoked, any non-ALP_OK return wipes the whole buffer per the discard contract. But early parameter-validation failuresALP_ERR_INVAL / ALP_ERR_NOT_READY, returned before the backend is ever called — leave plain_out untouched, because nothing of ours was ever written there. Do not read a ALP_ERR_INVAL return as "the buffer has been cleared for me": whatever your own code left in that buffer is still there. :::

On ALP_ERR_IO (tag mismatch) the message has been tampered with: plain_out content is undefined and must be discarded.

Random bytes

uint8_t nonce[12];
alp_random_bytes(nonce, sizeof(nonce));

On Zephyr / bare-metal this routes through the SoC's TRNG (or PSA's psa_generate_random()). On Yocto it routes through OpenSSL's RAND_bytes.

OPTIGA Trust M

Every Alp Lab SoM populates an Infineon OPTIGA Trust M secure element. It exposes pre-provisioned ECC key pairs for TLS client certs, ECDSA-P256 signing, and X.509 certificate storage. Direct access is via the chip driver:

#include <alp/chips/optiga_trust_m.h>

optiga_trust_m_t se;
optiga_trust_m_init(&se, brd_i2c, 0x30);

uint8_t sig[64];
optiga_trust_m_ecdsa_sign(&se, /* key_slot */ 0, digest, 32, sig);

<alp/security.h> integrates the OPTIGA into MbedTLS's PSA Crypto driver model so generic TLS code transparently uses it.

Secure boot + OTA

On AEN the MCUboot chain now verifies — it is not just present. The shared profile at zephyr/sysbuild/aen/sysbuild.conf sets SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y, and the MCUboot child image it produces carries CONFIG_BOOT_VALIDATE_SLOT0=y, so the bootloader checks the application's ECDSA-P256 signature in slot0 before chain-loading it.

This is measured on real E1M-AEN801 silicon (AE822FA0E5597LS0 Rev A0), not inferred from a successful boot: verification runs to completion at PC=80012FBC, VTOR=80010800, and flipping one byte of the TLV 0x22 signature yields E: Unable to find bootable image — a clean refusal rather than a hang. The verified backend is TinyCrypt (CONFIG_BOOT_ECDSA_TINYCRYPT=y).

Two constraints follow for anyone building against it:

  • The signing key lives in that shared profile, not in an example's own sysbuild.conf. Pass -DSB_CONF_FILE=<abs>/alp-sdk/zephyr/sysbuild/aen/sysbuild.conf (absolute path) to pick up SB_CONFIG_BOOT_SIGNATURE_KEY_FILE. Without it the build hard-fails at configure time.
  • SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=y is unchanged — there is no A/B swap on AEN, and the swap path is untested on this part.

See production-deployment for the full build recipe.

For chain-of-trust details (MCUboot on AEN-Zephyr, Mender on Yocto, key lifecycle), see the SDK repo:

See also

Questions about this page? Discuss in Community Forum