<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.
Header
#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
| Algorithm | Digest length |
|---|---|
ALP_HASH_SHA256 | 32 B |
ALP_HASH_SHA384 | 48 B |
ALP_HASH_SHA512 | 64 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.
| Return | Handle state | What to do |
|---|---|---|
ALP_OK | Closed for you | Do not call alp_hash_close — the digest is written. |
ALP_ERR_INVAL (digest_cap too small) | Left open and unchanged | digest_len receives the required length. Call again with a large-enough buffer, or alp_hash_close explicitly. |
| Any other failure | Left safely closeable | Always 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 failures — ALP_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 upSB_CONFIG_BOOT_SIGNATURE_KEY_FILE. Without it the build hard-fails at configure time. SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=yis 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
<alp/iot.h>— MQTT over TLS- Chip catalogue —
optiga_trust_m