Using Cryptography

lwIP-CE exposes a set of cryptographic primitives independently of the network stack. This section will go over the modules of the cryptography API, and end with some use-case examples.

Important

The calculator has no secure enclave or cryptographic acceleration. It has no concept of permissions, file ownership, or process ownership. There are no built-in controls to ensure file integrity or prevent arbitrary code execution. Please be aware of this when using TLS or any cryptographic module in this project. Operate under the premise that your calculator is your trust boundary.

Note

Call lwip_start() before using any lwIP-CE cryptography. This patches the LibLoad jump table so lwIP-CE calls are safe, and initializes the TRNG.

#include <lwip.h>       // for lwip_start()
#include <cryptography.h>

int main(void)
{
   if (!lwip_start())
      return 1;

   /* crypto primitives are ready — no network interface needed */
}

Include cryptography.h for the full set of primitives, or include individual headers from lwip/cryptography/ for a narrower surface.

The recommended API surface consists of:


TRNG

Header: lwip/cryptography/random.h
References: NIST SP 800-90A Rev.1 — DRBG; NIST SP 800-90B — Entropy assessment

The calculator has no hardware RNG. lwIP-CE derives entropy from SRAM noise — the electrical state of uninitialized SRAM varies between power cycles. The generator is designed in alignment with NIST SP 800-90 guidance. Testing indicates that the entropy source provides sufficient min-entropy for lwIP-CE’s cryptographic random-value requirements, with an estimated min-entropy of H∞ ≈ 0.99998 bits per output bit (≈ 1.00000 across the full entropy pool), with a median correlation factor of keff = 1.031, computed over a 1.2 MB nominal dataset per unit tested. For the full entropy analysis, see the whitepaper. Do not use the toolchain random() functions for anything security-sensitive; they are not cryptographically secure.

bool tls_random_init_entropy(void)

Initialize the cryptographic TRNG. Selects an SRAM entropy source, runs the conditioning pass, and seeds the DRBG. Must be called before tls_random() or tls_random_bytes() unless lwip_start() has already been called (which initializes entropy automatically).

Returns:

true on success, false if entropy initialization failed (e.g. no suitable SRAM source found).

bool tls_rng_healthcheck(void)

Run one immediate health-check cycle — the same lightweight sanity and recovery logic used by the periodic RNG health timer. If repeated failures have been detected, this may trigger a re-initialization of the entropy source. Always call this before generating random data.

Returns:

true if the RNG is healthy and safe to use, false otherwise. On false, retry tls_random_init_entropy() or abort.

void *tls_random_bytes(void *buffer, size_t len)

Fill buffer with len cryptographically random bytes, synchronously. The health check must pass before calling this.

Parameters:
  • buffer – Caller-supplied destination buffer.

  • len – Number of bytes to fill.

Returns:

buffer on success.

uint64_t tls_random(void)

Return a single cryptographically random 64-bit value, synchronously. The health check must pass before calling this.

Returns:

A 64-bit random integer.

bool tls_request_random_bytes(uint8_t *out, size_t len, tls_random_request_cb_t cb, void *arg, bool blocking)

Request len random bytes into out, either synchronously or via lwIP timer callbacks. Only one request can be active at a time — check tls_rng_is_busy() before calling. The caller must keep out valid until the request completes (i.e. until cb is invoked in async mode).

Parameters:
  • out – Destination buffer. Must remain valid until completion.

  • len – Number of random bytes requested.

  • cb – Optional completion callback (void cb(bool ok, void *arg)). ok is true on success.

  • arg – Opaque pointer passed through to cb.

  • blocking – true to gather entropy immediately in the caller (may block for a long time); false to gather in chunks via lwip_service_events() and invoke cb when done.

Returns:

true if the request started or completed successfully, false on failure (busy, bad args, or health failure).

bool tls_rng_is_busy(void)

Returns true if a tls_request_random_bytes() request is currently in progress. Use this to guard against starting a second request before the first completes.

Returns:

true if busy, false if idle.

Simple synchronous use:

/* generator already initialized by lwip_start() */
uint8_t key[32];

if(tls_rng_healthcheck())              /* always ensure healthiness before generating */
   tls_random_bytes(key, sizeof(key)); /* fill with cryptographic random bytes */
else printf("error: rng");
/* ^ You'll want to actually handle this (ex: retry tls_random_init_entropy()) */

// ... a bit later ...
uint64_t r;
if(tls_rng_healthcheck())
   r = tls_random();                  /* get a single random 64-bit value */
else printf("error: rng");
/* ^ You'll want to actually handle this (ex: retry tls_random_init_entropy()) */

After lwip_start(), the entropy source is initialized automatically as part of the TLS subsystem setup; calling tls_random_init_entropy() again after that is harmless but redundant.

Async gathering (large requests, main-loop friendly):

static void on_random(bool ok, void *arg) {
    /* called from lwip_service_events() when entropy is ready */
}

uint8_t entropy_buf[64];
tls_request_random_bytes(entropy_buf, sizeof(entropy_buf),
                         on_random, NULL,
                         false);  /* false = async via lwIP timers */

/* OR: blocking (may stall the UI for a moment) */
tls_request_random_bytes(entropy_buf, sizeof(entropy_buf),
                         NULL, NULL,
                         true);   /* true = gather immediately */

Only one request can be in flight at a time. tls_rng_is_busy() returns true if a request is active. The out buffer must remain valid until the callback fires (async) or the call returns (blocking).

tls_rng_healthcheck() runs the same lightweight sanity check the periodic internal timer runs. It returns true if the RNG health is currently acceptable.

Hashing and HMAC

Hashing

Headers: lwip/cryptography/hash.h
References: FIPS 180-4 — SHA-256

A hash function produces a fixed-size digest of its input. An HMAC (hash-based message authentication code) folds a secret key into the hash so a valid tag cannot be reproduced without the key.

SHA-256 is the only implemented algorithm. TLS_SHA256_DIGEST_LEN (32) is the digest length.

bool tls_hash_context_init(struct tls_hash_context *ctx, uint8_t algorithm)

Initialize a generic hash context to the specified algorithm.

Parameters:
  • ctx – Caller-allocated generic hash context.

  • algorithm – TLS_HASH_SHA256 (the only implemented value).

Returns:

true on success, false if the algorithm is unrecognized.

void tls_hash_update(struct tls_hash_context *ctx, const uint8_t *data, size_t len)

Feed data into a generic hash context.

Parameters:
  • ctx – Initialized generic hash context.

  • data – Input data.

  • len – Length of input data.

void tls_hash_digest(struct tls_hash_context *ctx, uint8_t *digest)

Finalize and write the digest from a generic hash context.

Parameters:
  • ctx – Initialized generic hash context with data fed in.

  • digest – Output buffer sized to the algorithm’s digest length (TLS_SHA256_DIGEST_LEN = 32 bytes for SHA-256).

bool tls_mgf1(const uint8_t *data, size_t datalen, uint8_t *outbuf, size_t outlen, uint8_t hash_alg)

Compute an MGF1 mask. Used internally by RSA-OAEP and RSA-PSS; exposed for callers that need the same mask generation function.

Parameters:
  • data – Seed data.

  • datalen – Length of seed.

  • outbuf – Output mask buffer.

  • outlen – Desired mask length in bytes.

  • hash_alg – Hash algorithm to use (TLS_HASH_SHA256).

Returns:

true on success, false on error.

uint8_t digest[TLS_SHA256_DIGEST_LEN];
struct tls_hash_context hctx;

tls_hash_context_init(&hctx, TLS_HASH_SHA256);
tls_hash_update(&hctx, data, len);
tls_hash_digest(&hctx, digest);

HMAC

Headers: lwip/cryptography/hmac.h
References: RFC 2104 — HMAC

HMAC wraps the hash context with a key. The interface mirrors the hash API: init with a key, feed data with update, retrieve the MAC with digest. The context is caller-allocated and requires no teardown.

bool tls_hmac_context_init(struct tls_hmac_context *ctx, uint8_t algorithm, const uint8_t *key, size_t keylen)

Initialize an HMAC context with a key and hash algorithm.

Parameters:
  • ctx – Caller-allocated HMAC context.

  • algorithm – TLS_HASH_SHA256.

  • key – HMAC key.

  • keylen – Key length in bytes.

Returns:

true on success, false on error.

void tls_hmac_update(struct tls_hmac_context *ctx, const uint8_t *data, size_t len)

Feed data into an HMAC context. May be called any number of times.

Parameters:
  • ctx – Initialized HMAC context.

  • data – Input data.

  • len – Length of input data.

void tls_hmac_digest(struct tls_hmac_context *ctx, uint8_t *digest)

Finalize and write the MAC. The context state is consumed; call tls_hmac_context_init() again before reuse.

Parameters:
  • ctx – Initialized HMAC context with data fed in.

  • digest – Output buffer (TLS_SHA256_DIGEST_LEN = 32 bytes for HMAC-SHA-256).

struct tls_hmac_context ctx;
uint8_t mac[TLS_SHA256_DIGEST_LEN];

tls_hmac_context_init(&ctx, TLS_HASH_SHA256, key, key_len);
tls_hmac_update(&ctx, data, len);
tls_hmac_digest(&ctx, mac);

Key Derivation

HKDF

Headers: lwip/cryptography/hkdf.h
References: RFC 5869 — HKDF

HKDF derives keying material from an input secret in two steps: extract condenses potentially weak or non-uniform input keying material (IKM) into a pseudorandom key (PRK), then expand stretches the PRK into output keying material of any desired length. TLS 1.3-specific label expansion is provided as a convenience. All functions are stateless — no context struct, each call is independent.

bool tls_hkdf_extract(uint8_t hash_algorithm, const uint8_t *salt, size_t salt_len, const uint8_t *ikm, size_t ikm_len, uint8_t *prk)

Extract a fixed-length pseudorandom key from input keying material. PRK = HMAC-Hash(salt, IKM).

Parameters:
  • hash_algorithm – TLS_HASH_SHA256.

  • salt – Optional salt. Pass NULL and 0 for no salt.

  • salt_len – Length of salt in bytes.

  • ikm – Input keying material.

  • ikm_len – Length of IKM in bytes.

  • prk – Output PRK buffer (TLS_SHA256_DIGEST_LEN = 32 bytes for SHA-256).

Returns:

true on success, false on failure.

bool tls_hkdf_expand(uint8_t hash_algorithm, const uint8_t *prk, size_t prk_len, const uint8_t *info, size_t info_len, uint8_t *okm, size_t okm_len)

Expand a PRK to the desired output length. OKM = HKDF-Expand(PRK, info, L).

Parameters:
  • hash_algorithm – TLS_HASH_SHA256.

  • prk – Pseudorandom key from tls_hkdf_extract().

  • prk_len – Length of PRK (typically the hash digest length).

  • info – Optional context info. Pass NULL and 0 for none.

  • info_len – Length of info.

  • okm – Output keying material buffer.

  • okm_len – Desired output length in bytes (max: 255 * hash_len).

Returns:

true on success, false on failure.

bool tls_hkdf_expand_label(uint8_t hash_algorithm, const uint8_t *secret, size_t secret_len, const char *label, size_t label_len, const uint8_t *context, size_t context_len, uint8_t *out, size_t out_len)

TLS 1.3 HKDF-Expand-Label. Formats the label with the "tls13 " prefix per RFC 8446 before calling Expand.

Parameters:
  • hash_algorithm – TLS_HASH_SHA256.

  • secret – Input secret.

  • secret_len – Length of secret.

  • label – ASCII label string, without the "tls13 " prefix.

  • label_len – Length of label.

  • context – Optional context (typically a transcript hash). Pass NULL and 0 for none.

  • context_len – Length of context.

  • out – Output buffer.

  • out_len – Desired output length in bytes.

Returns:

true on success, false on failure.

bool tls_derive_secret(uint8_t hash_algorithm, const uint8_t *secret, size_t secret_len, const char *label, size_t label_len, const uint8_t *transcript_hash, size_t transcript_hash_len, uint8_t *out)

TLS 1.3 Derive-Secret. Convenience wrapper equivalent to HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length).

Parameters:
  • hash_algorithm – TLS_HASH_SHA256.

  • secret – Input secret.

  • secret_len – Length of secret.

  • label – ASCII label string.

  • label_len – Length of label.

  • transcript_hash – Hash of the handshake transcript.

  • transcript_hash_len – Length of transcript hash (32 for SHA-256).

  • out – Output buffer (hash_len bytes).

Returns:

true on success, false on failure.

uint8_t prk[TLS_SHA256_DIGEST_LEN];
uint8_t okm[42];

/* Step 1: extract */
tls_hkdf_extract(TLS_HASH_SHA256,
                 salt, salt_len,   /* NULL/0 for no salt */
                 ikm, ikm_len,
                 prk);

/* Step 2: expand */
tls_hkdf_expand(TLS_HASH_SHA256,
                prk, sizeof(prk),
                info, info_len,   /* NULL/0 for no info */
                okm, sizeof(okm));

PBKDF2

Headers: lwip/cryptography/passwords.h
References: RFC 8018 — PBKDF2

PBKDF2 derives a key from a password and salt by iterating HMAC many times. More rounds increase resistance to brute-force, but on the eZ80 this is measurably slow. Benchmark the round count against your application’s latency budget and password-protection requirements. The output buffer is caller-allocated; no heap allocation occurs.

bool tls_pbkdf2(const char *password, size_t passlen, const uint8_t *salt, size_t saltlen, uint8_t *key, size_t keylen, size_t rounds, uint8_t algorithm)

Derive a key from a password using PBKDF2-HMAC.

Parameters:
  • password – Password string.

  • passlen – Length of password in bytes.

  • salt – Salt value.

  • saltlen – Length of salt in bytes.

  • key – Output key buffer.

  • keylen – Desired key length in bytes.

  • rounds – HMAC iterations per output block. Higher is slower and more secure.

  • algorithm – Hash algorithm (TLS_HASH_SHA256).

Returns:

true on success, false on failure.

uint8_t key[32];
tls_pbkdf2("password", 8,
           salt, salt_len,
           key, sizeof(key),
           100,           /* iteration count */
           TLS_HASH_SHA256);

Key Operations

Header: lwip/cryptography/key.h

The Key API stores typed key material in struct tls_key. Import infers type from the encoded input; it does not select an operation. Verification, signing, encryption, and decryption take a separate alg argument to select the signature scheme or cipher.

Allocating and Setting a Key

A struct tls_key is a self-describing key container. The type field identifies the key material stored in the union. The algorithm (tls_alg_t) is a separate concept — it is chosen per operation, not stored in the key. Keys can be allocated by the caller and populated by hand, or allocated and populated by lwIP-CE via tls_key_import().

Key types and algorithms are related but orthogonal: type says what material is held; alg says what operation to perform. An RSA key can be used with TLS_ALG_RSA_PKCS1_SHA256 or TLS_ALG_RSA_PSS_RSAE_SHA256; an AES-128 key can be used with GCM, CCM, or CBC. Operations reject a key whose type is incompatible with the requested alg.

type tls_key_type_t

Identifies the key material stored in a struct tls_key. Set this to match whichever union member you populate.

TLS_KEY_TYPE_UNKNOWN

Zero value; denotes an empty or uninitialised key. Operations reject keys of this type.

TLS_KEY_TYPE_RSA

RSA key. Populate the rsa union member. Compatible with TLS_ALG_RSA_PKCS1_SHA256, TLS_ALG_RSA_PSS_RSAE_SHA256, and TLS_ALG_RSA_OAEP_SHA256.

TLS_KEY_TYPE_EC_P256

EC P-256 key. Populate the ec union member. Compatible with TLS_ALG_ECDSA_SECP256R1_SHA256 (currently unimplemented).

TLS_KEY_TYPE_AES

AES key. Populate the aes union member. Compatible with all TLS_ALG_AES_* ciphers; the key length (aes.len) must match the selected algorithm (16 bytes for _128_*, 32 bytes for _256_*).

struct tls_key

Self-describing key. Populate type and exactly one union member.

tls_key_type_t type

Key material type. Drives dispatch — operations check this against the requested alg and reject mismatches before touching key material. Must be set correctly before passing the key to any operation.

bool allocated

true when this struct was returned by tls_key_import() and must be freed with tls_key_free(). Leave false for stack-allocated or externally-managed keys. Do not set this flag manually.

struct tls_rsa_key rsa

Valid when type == TLS_KEY_TYPE_RSA. Contains modulus, mod_len, exponent, and exp_len — unsigned big-endian byte pointers and their lengths. Strip the DER INTEGER sign-padding byte when constructing RSA keys manually. The backend supports 128–256-byte moduli and exponents of at most three bytes.

struct { size_t len; const uint8_t *data; } ec

Valid when type == TLS_KEY_TYPE_EC_P256. Raw EC key bytes. For public keys this is the uncompressed point (04 || X || Y from the SPKI BIT STRING). For private keys this is the raw private scalar.

struct { size_t len; const uint8_t *data; } aes

Valid when type == TLS_KEY_TYPE_AES. Points to the raw key bytes; len must be 16 (128-bit) or 32 (256-bit) to match the selected cipher.

Stack-allocated keys borrow their buffers — keep those buffers alive for every operation. tls_key_free() is a no-op on a caller-allocated key because allocated is false.

tls_key_import_result_t tls_key_import(struct tls_key **out, const void *data, size_t len, tls_key_format_t format, const char *password)

Import encoded key material into a new heap allocation. The returned key owns a copy of its data, so the input buffer can be released after import. The type field is set from the encoded key type (RSA → TLS_KEY_TYPE_RSA, EC → TLS_KEY_TYPE_EC_P256). The algorithm (tls_alg_t) is not stored in the key — it is chosen per operation.

AES keys are not supported — there is no standard encoding for a raw symmetric key. Construct AES keys directly on the stack instead.

Parameters:
  • out – Receives the allocated key on success; set to NULL on error.

  • data – PEM text or DER bytes.

  • len – Input length in bytes.

  • format – TLS_KEY_FORMAT_PEM or TLS_KEY_FORMAT_DER.

  • password – Passphrase for an encrypted PKCS#8 key; NULL for unencrypted input.

Returns:

TLS_KEY_IMPORT_OK on success. Failures include TLS_KEY_IMPORT_INVALID_ARG, TLS_KEY_IMPORT_ALLOC_FAIL, TLS_KEY_IMPORT_PARSE_FAIL, TLS_KEY_IMPORT_BAD_ALG, TLS_KEY_IMPORT_UNSUPPORTED_ENC, and TLS_KEY_IMPORT_DECRYPT_FAIL.

It follows that there are two ways to define a tls_key struct. The code below will illustrate both.

uint8_t aes_key[16] = { /* this should be TRNG output */ };
struct tls_key my_aes_key = {
    .type      = TLS_KEY_TYPE_AES,
    .allocated = false,
    .aes       = { .len = sizeof(aes_key), .data = aes_key },
};

/* ... a bit later ... */
uint8_t f = ti_Open("KeyFile", "r");
if(f){
   struct tls_key *my_rsa_key = NULL;
   const char *passwd = "/* User input here */";
   if(tls_key_import(&my_rsa_key,
                     ti_GetDataPtr(f), ti_GetSize(f),
                     TLS_KEY_FORMAT_PEM, passwd) != TLS_KEY_IMPORT_OK)
      printf("error");
   ti_Close(f);
   /* ... use my_rsa_key ... */
   tls_key_free(my_rsa_key);
}

When constructing a key manually:

  • Set type to the appropriate TLS_KEY_TYPE_* constant.

  • For AES, set aes.data to the key bytes and aes.len to 16 or 32, matching the _128_* or _256_* algorithm you intend to use.

  • For RSA, set rsa.modulus and rsa.exponent to unsigned big-endian byte arrays with lengths in rsa.mod_len and rsa.exp_len. Strip the DER INTEGER sign-padding byte. The backend supports 128–256-byte moduli and exponents of at most three bytes.

  • For EC, set ec.data to the uncompressed public point and ec.len to its length (65 bytes for P-256).

Manually constructed keys borrow their buffers. Keep those buffers alive for every operation and leave allocated false. Zero key material with tls_secure_memzero() when it is no longer needed. Operation calls reject an incompatible type or AES key length before accessing key material. TLS_ALG_IS_SIGNING() and TLS_ALG_IS_ENCRYPTION() classify algorithm ranges; they do not establish that an operation is implemented.

Encryption/Decryption over a Key

Encrypt and decrypt operations use a self-describing blob format so that no caller-managed buffers are required. The encrypt blob is a flat byte array:

<uint16_t iv_len> <iv bytes> <uint16_t ct_len> <ciphertext bytes> <uint16_t tag_len> <tag bytes>

All uint16_t fields are little-endian. iv_len is 0 for RSA-OAEP. tag_len is 0 for CBC and RSA-OAEP (no authentication tag).

The decrypt blob contains the recovered plaintext:

<uint16_t plain_len> <plaintext bytes>

Both blob types are freed with the same function: tls_cipher_blob_free(). The blobs are self-describing, so an externally received ciphertext can be decoded into this format and passed to tls_cipher_decrypt() without having originated from tls_cipher_encrypt().

Note

AAD is not included in the ciphertext blob. It is authenticated by the tag but never encrypted or stored. If you used tls_cipher_encrypt_aad(), you must transmit the AAD to the recipient through a separate channel and pass the identical bytes to tls_cipher_decrypt_aad(). A mismatch causes tag verification to fail and decrypt returns NULL.

uint8_t *tls_cipher_encrypt(const struct tls_key *key, tls_alg_t alg, const uint8_t *in, size_t in_len)

Encrypt in and return an allocated ciphertext blob. A fresh random IV is generated internally. The RNG health check runs before generating the IV.

Parameters:
  • key – Key to encrypt with; alg and key type must be compatible.

  • alg – Cipher to use (e.g. TLS_ALG_AES_256_GCM).

  • in – Plaintext input.

  • in_len – Plaintext length in bytes.

Returns:

Allocated ciphertext blob on success, NULL on any error (bad args, RNG failure, allocation failure, or cipher error). Free with tls_cipher_blob_free().

uint8_t *tls_cipher_encrypt_aad(const struct tls_key *key, tls_alg_t alg, const uint8_t *aad, size_t aad_len, const uint8_t *in, size_t in_len)

Encrypt with additional authenticated data (AAD). The AAD is authenticated but not encrypted; supply the identical bytes to the matching decrypt call. RSA-OAEP and CBC do not support AAD — passing a non-NULL aad or a non-zero aad_len with those algorithms returns NULL. Otherwise identical to tls_cipher_encrypt().

Parameters:
  • aad – Additional authenticated data. Pass NULL and 0 for none.

  • aad_len – Length of AAD in bytes.

Returns:

Allocated ciphertext blob on success, NULL on error. Free with tls_cipher_blob_free().

uint8_t *tls_cipher_decrypt(const struct tls_key *key, tls_alg_t alg, const uint8_t *blob)

Decrypt a ciphertext blob and return an allocated plaintext blob. For GCM/CCM the authentication tag is verified before decrypting; returns NULL on mismatch. Use the same alg that was passed to encrypt. blob need not have come from tls_cipher_encrypt(); any correctly formatted blob is accepted.

Parameters:
  • key – Key to decrypt with.

  • alg – Cipher used to encrypt.

  • blob – Self-describing ciphertext blob.

Returns:

Allocated plaintext blob on success, NULL on any error or tag mismatch. The plaintext is at blob + 2 and its length is the little-endian uint16_t at blob[0..1]. Free with tls_cipher_blob_free().

uint8_t *tls_cipher_decrypt_aad(const struct tls_key *key, tls_alg_t alg, const uint8_t *aad, size_t aad_len, const uint8_t *blob)

Decrypt with AAD. The AAD is fed into tag verification before any bytes are decrypted; a mismatch returns NULL. RSA-OAEP and CBC do not support AAD — passing a non-NULL aad or a non-zero aad_len with those algorithms returns NULL. Otherwise identical to tls_cipher_decrypt().

Parameters:
  • aad – Additional authenticated data; must match what was passed to encrypt.

  • aad_len – Length of AAD in bytes.

Returns:

Allocated plaintext blob on success, NULL on error or mismatch. Free with tls_cipher_blob_free().

uint8_t *tls_cipher_blob_assemble(const uint8_t *iv, size_t iv_len, const uint8_t *ct, size_t ct_len, const uint8_t *tag, size_t tag_len)

Assemble a ciphertext blob from separately held fields. Use this when the IV, ciphertext, and tag arrive as distinct buffers (e.g. received over the network) and need to be packed into the format expected by tls_cipher_decrypt() / tls_cipher_decrypt_aad().

Any field may be NULL with a corresponding length of 0 (e.g. RSA-OAEP has no IV or tag; CBC has no tag).

Parameters:
  • iv – IV/nonce bytes, or NULL.

  • iv_len – Length of iv in bytes.

  • ct – Ciphertext bytes.

  • ct_len – Length of ct in bytes.

  • tag – Authentication tag bytes, or NULL.

  • tag_len – Length of tag in bytes.

Returns:

Allocated ciphertext blob on success, NULL on allocation failure. Free with tls_cipher_blob_free().

Encryption ciphers and support

Cipher

Member

Operation

TLS_ALG_AES_128_GCM, TLS_ALG_AES_256_GCM

aes

Authenticated encryption/decryption; 12-byte IV, 16-byte tag.

TLS_ALG_AES_128_CCM, TLS_ALG_AES_256_CCM

aes

Authenticated encryption/decryption; 13-byte nonce, 16-byte tag.

TLS_ALG_AES_128_CBC, TLS_ALG_AES_256_CBC

aes

Encryption/decryption; 16-byte IV, no authentication tag.

TLS_ALG_RSA_OAEP_SHA256

rsa

RSA-OAEP encryption with SHA-256; see decryption limitations below.

Example — AES-128-GCM round-trip

/* Stack-allocated AES key. */
static const uint8_t raw_key[16] = { /* 16 key bytes */ };
struct tls_key k = {
    .type     = TLS_KEY_TYPE_AES,
    .aes      = { .data = raw_key, .len = sizeof raw_key },
    .allocated = false,
};

/* Encrypt. */
const uint8_t plaintext[] = "hello";
uint8_t *enc = tls_cipher_encrypt(&k, TLS_ALG_AES_128_GCM,
                                plaintext, sizeof plaintext);
if (!enc) { /* handle error */ }

/* Decrypt. */
uint8_t *dec = tls_cipher_decrypt(&k, TLS_ALG_AES_128_GCM, enc);
if (!dec) { /* tag mismatch or error */ }

/* Parse the plaintext from the decrypt blob. */
uint16_t plain_len = (uint16_t)(dec[0] | ((uint16_t)dec[1] << 8));
const uint8_t *pt  = dec + 2;
/* use pt[0..plain_len-1] here */

tls_cipher_blob_free(enc);
tls_cipher_blob_free(dec);

Signing and Verifying

Result type

type tls_key_op_result_t

Return value for tls_key_verify() and tls_key_sign().

TLS_KEY_OP_OK

Operation succeeded.

TLS_KEY_OP_INVALID

Operation failed: bad signature, key/algorithm mismatch, or wrong key type for the requested algorithm.

TLS_KEY_OP_UNSUPPORTED

Algorithm is recognized but not yet implemented (e.g. ECDSA, RSA signing).

TLS_KEY_OP_UNKNOWN

Algorithm value is out of range for the requested operation category (e.g. an encryption algorithm passed to tls_key_verify()).

Functions

tls_key_op_result_t tls_key_verify(const uint8_t *content, size_t content_len, const uint8_t *sig, size_t sig_len, const struct tls_key *key, tls_alg_t alg)

Verify a signature over content using key.

Hashes content with SHA-256 internally; pass the raw message, not a pre-computed digest. Dispatches on alg after checking key->type; returns TLS_KEY_OP_INVALID when key->type does not match alg.

Only signing-range algorithms are accepted (TLS_ALG_IS_SIGNING). Passing an encryption-range alg returns TLS_KEY_OP_UNKNOWN.

Parameters:
  • content – Message bytes to verify.

  • content_len – Length of content in bytes.

  • sig – Raw signature bytes.

  • sig_len – Length of sig in bytes.

  • key – Key holding the public material; key->type must match alg.

  • alg – Signature scheme to use (e.g. TLS_ALG_RSA_PKCS1_SHA256).

Returns:

TLS_KEY_OP_OK on valid signature; TLS_KEY_OP_INVALID, TLS_KEY_OP_UNSUPPORTED, or TLS_KEY_OP_UNKNOWN otherwise.

tls_key_op_result_t tls_key_sign(const uint8_t *content, size_t content_len, uint8_t *sig_out, size_t *sig_len_out, const struct tls_key *key, tls_alg_t alg)

Sign content with key, writing the raw signature to sig_out.

Hashes content with SHA-256 internally; pass the raw message, not a pre-computed digest. sig_len_out receives the number of bytes written on success. The caller must pre-allocate sig_out large enough to hold the output (RSA signatures are key->rsa.mod_len bytes).

Note

Signing is currently unimplemented for all algorithms, including RSA. All calls return TLS_KEY_OP_UNSUPPORTED until private-key signing is added.

Parameters:
  • content – Message bytes to sign.

  • content_len – Length of content in bytes.

  • sig_out – Caller-allocated buffer to receive the raw signature.

  • sig_len_out – Receives the number of bytes written on success.

  • key – Key holding the private material.

  • alg – Signature scheme to use.

Returns:

TLS_KEY_OP_OK on success; TLS_KEY_OP_UNSUPPORTED until signing is implemented; TLS_KEY_OP_INVALID or TLS_KEY_OP_UNKNOWN on bad arguments.

Signature schemes and support

Algorithm

Member

Operation

TLS_ALG_RSA_PKCS1_SHA256

rsa

Verify PKCS#1 v1.5 signatures with SHA-256.

TLS_ALG_RSA_PSS_RSAE_SHA256

rsa

Verify PSS signatures with SHA-256, MGF1-SHA-256 and a 32-byte salt.

TLS_ALG_ECDSA_SECP256R1_SHA256

ec

Recognized, but verification and signing are unimplemented.

Example:

struct tls_key *pub = ...; /* imported RSA public key */
const uint8_t *msg = (const uint8_t *)"hello";
const uint8_t sig[128] = { /* signature bytes from peer */ };

tls_key_op_result_t r = tls_key_verify(msg, 5, sig, sizeof(sig),
                                        pub, TLS_ALG_RSA_PKCS1_SHA256);
if (r != TLS_KEY_OP_OK) {
    /* signature invalid or algorithm not supported */
}

Freeing Keys

There are two distinct lifetimes to manage: the struct tls_key itself (heap-allocated by tls_key_import()) and any cipher blobs produced by encrypt or decrypt operations.

void tls_key_free(struct tls_key *key)

Zero and release a key returned by tls_key_import().

Checks key->allocated before doing anything: if false the function returns immediately, making it safe to call on stack-allocated or externally-managed keys. When true, zeroes the entire allocation (key header plus trailing key material) and frees it.

Safe to call with NULL.

Warning

Do not call tls_key_free() on the pubkey member embedded inside a struct tls_x509_object. That key borrows its buffers from the certificate allocation. Free the owning certificate object instead.

Note

Stack-allocated keys (allocated = false) borrow their buffers from the caller. Use tls_secure_memzero() to zero those buffers when the key is no longer needed — tls_key_free() will not touch them.

void tls_cipher_blob_free(uint8_t *buf)

Zero and free a blob returned by tls_cipher_encrypt(), tls_cipher_encrypt_aad(), tls_cipher_decrypt(), tls_cipher_decrypt_aad(), or tls_cipher_blob_assemble().

The allocator records the full allocation size in a hidden header, so the blob is zeroed in its entirety (IV, ciphertext, tag, and plaintext for decrypt blobs) before the memory is released.

Safe to call with NULL.

Summary of ownership rules:

  • tls_key_import() → free with tls_key_free().

  • Stack struct tls_key → call tls_secure_memzero() on borrowed buffers yourself; never pass to tls_key_free().

  • tls_cipher_encrypt() / tls_cipher_decrypt() / tls_cipher_blob_assemble() → free with tls_cipher_blob_free().

  • struct tls_x509_object → free with tls_x509_object_free(); never free its embedded pubkey independently.


Certificates

A struct tls_x509_object holds the parsed fields of a single X.509 certificate — distinguished names, validity window, extensions, and the extracted public key. All pointer members reference DER bytes; the object does not own independent copies of each field.

There are two ways to obtain one, with different ownership rules:

  • Standalone import via tls_x509_import_certificate(): allocates a single heap block (object header + trailing DER copy). Pointer members reference that trailing copy. Must be released with tls_x509_object_free().

  • Parse-in-place via tls_x509_parse_certificate(): populates a caller-allocated struct tls_x509_object with pointers into the caller-supplied DER buffer. No heap allocation; the caller keeps the DER buffer alive. tls_x509_object_free() is a no-op on these (from_handshake is true).

In both cases pubkey.allocated is always false — the key material lives inside the DER bytes, not in a separate allocation. Never call tls_key_free() on obj->pubkey; use tls_x509_object_free() on the certificate instead.

The Object

struct tls_x509_object

Parsed X.509 certificate. All pointer members reference DER bytes.

bool from_handshake

true when populated by tls_x509_parse_certificate() (pointers borrow the caller’s buffer; tls_x509_object_free() is a no-op). false when allocated by tls_x509_import_certificate() (must be freed).

const uint8_t *issuer_cn
size_t issuer_cn_len

Issuer CommonName — raw string bytes (no NUL terminator).

const uint8_t *subject_cn
size_t subject_cn_len

Subject CommonName — raw string bytes (no NUL terminator).

const uint8_t *not_before
size_t not_before_len
uint8_t not_before_tag

RFC 5280 notBefore field: raw value bytes and the ASN.1 tag (ASN1_UTCTIME or ASN1_GENERALIZEDTIME). The certificate is not valid before this time.

const uint8_t *not_after
size_t not_after_len
uint8_t not_after_tag

RFC 5280 notAfter field: raw value bytes and ASN.1 tag. The certificate is not valid after this time.

const uint8_t *extensions
size_t extensions_len

Raw bytes of the SEQUENCE OF Extension inside the [3] EXPLICIT extensions wrapper; NULL / 0 when no extensions are present.

struct tls_key pubkey

Public key extracted from SubjectPublicKeyInfo. pubkey.allocated is always false; never pass to tls_key_free().

size_t der_len
uint8_t der[]

Heap-owned DER copy (only meaningful when !from_handshake). The flexible array member trails the struct in the same allocation.

Importing and Freeing

struct tls_x509_object *tls_x509_import_certificate(const char *pem_data, size_t size)

Parse a PEM-encoded certificate and return an allocated tls_x509_object. Allocates a single block: object header plus a DER copy; all pointer members reference that trailing DER. The input pem_data buffer can be released after this call returns.

Parameters:
  • pem_data – PEM text starting with -----BEGIN CERTIFICATE-----.

  • size – Length of pem_data in bytes.

Returns:

Allocated object on success, NULL on parse or allocation failure. Free with tls_x509_object_free().

bool tls_x509_parse_certificate(const uint8_t *cert_der, size_t cert_len, struct tls_x509_object *out)

Parse a DER-encoded certificate into a caller-allocated struct tls_x509_object. All pointer members in out reference bytes inside cert_der; the caller must keep cert_der live for the entire lifetime of out. Sets out->from_handshake = true.

Parameters:
  • cert_der – DER-encoded certificate bytes.

  • cert_len – Length of cert_der in bytes.

  • out – Caller-allocated object to receive parsed fields.

Returns:

true on success, false on parse failure.

void tls_x509_object_free(struct tls_x509_object *obj)

Release a certificate object allocated by tls_x509_import_certificate(). Zeroes the entire allocation (header + DER copy) before freeing.

No-op when obj->from_handshake is true (parse-in-place objects borrow their DER; the caller owns that buffer). Safe to call with NULL.

Validation Helpers

bool tls_x509_hostname_matches(const uint8_t *ext_data, size_t ext_len, const uint8_t *subject_cn, size_t subject_cn_len, const char *hostname)

Check whether a leaf certificate covers hostname.

Walks the subjectAltName extension for dNSName entries, matching each against hostname with ASCII case-insensitive comparison and single leading-label wildcard support (*.example.com matches foo.example.com but not foo.bar.example.com or example.com itself). Falls back to matching the subject CommonName when no subjectAltName extension is present (RFC 6125 legacy). Fails closed on any parse error.

IP literals require an exact binary iPAddress SAN match (IPv4 or IPv6). Neither DNS SANs nor CommonName can authenticate an IP destination. IPv6 literals must be unbracketed and have no zone identifier. TLS omits SNI when connecting to an IP literal.

Parameters:
  • ext_data – Raw bytes of obj->extensions.

  • ext_len – obj->extensions_len.

  • subject_cn – obj->subject_cn for CN fallback; may be NULL.

  • subject_cn_len – obj->subject_cn_len.

  • hostname – NUL-terminated hostname the connection was made to.

Returns:

true if the certificate covers hostname.

bool tls_x509_time_in_validity(const struct tls_x509_object *cert, uint32_t now_secs)

Check whether now_secs falls within the certificate’s notBefore–notAfter window. Reads validity fields directly from the parsed object. Fails closed on any parse error or inverted validity window.

Parameters:
  • cert – Parsed certificate.

  • now_secs – Current time as a Unix timestamp (seconds since epoch).

Returns:

true if now_secs is within [notBefore, notAfter].

bool tls_x509_time_to_unix(const uint8_t *data, size_t len, uint8_t tag, uint32_t *out_secs)

Convert a raw ASN.1 UTCTime or GeneralizedTime value to a Unix timestamp. Pass the value bytes and tag directly from a tls_x509_object validity field. Fails closed on malformed input.

Parameters:
  • data – Raw value bytes (the TLV value, not the tag or length).

  • len – Length of data.

  • tag – ASN.1 tag byte (ASN1_UTCTIME or ASN1_GENERALIZEDTIME).

  • out_secs – Receives the Unix timestamp on success.

Returns:

true on success.

Signature Verification

These functions verify signatures in certificate-processing contexts. For verifying application signatures against an imported key use tls_key_verify() instead.

tls_key_op_result_t tls_x509_signature_verify(const uint8_t *content, size_t content_len, const uint8_t *sig, size_t sig_len, const struct tls_key *key, tls_alg_t alg)

Verify a signature over arbitrary content using a key and an explicit scheme. SHA-256 hashes content internally before verifying.

Dispatches on alg:

  • TLS_ALG_RSA_PKCS1_SHA256 — RSASSA-PKCS1-v1.5 SHA-256

  • TLS_ALG_RSA_PSS_RSAE_SHA256 — RSASSA-PSS SHA-256 (saltLen=32)

  • TLS_ALG_ECDSA_SECP256R1_SHA256 — returns TLS_KEY_OP_UNSUPPORTED

  • Encryption-range or unknown alg — returns TLS_KEY_OP_UNKNOWN

Parameters:
  • content – Signed data (e.g. DER TBSCertificate bytes).

  • content_len – Length of content.

  • sig – Raw signature bytes.

  • sig_len – Length of sig.

  • key – Self-describing key; key->type must match alg.

  • alg – Signature scheme to verify.

Returns:

TLS_KEY_OP_OK on valid signature; TLS_KEY_OP_INVALID, TLS_KEY_OP_UNSUPPORTED, or TLS_KEY_OP_UNKNOWN otherwise.

tls_key_op_result_t tls_x509_signature_verify_digest(const uint8_t digest[32], const uint8_t *sig, size_t sig_len, const struct tls_key *key, tls_alg_t alg)

Verify a signature over a pre-computed SHA-256 digest. Same dispatch as tls_x509_signature_verify() but accepts a 32-byte digest instead of raw content. Used when the TBS bytes are no longer available (e.g. a certificate chain walker that pre-hashes and then discards the TBS).

Parameters:
  • digest – 32-byte SHA-256 digest of the signed content.

  • sig – Raw signature bytes.

  • sig_len – Length of sig.

  • key – Self-describing key.

  • alg – Signature scheme to verify.

Returns:

TLS_KEY_OP_OK on success; TLS_KEY_OP_INVALID, TLS_KEY_OP_UNSUPPORTED, or TLS_KEY_OP_UNKNOWN otherwise.


Byte Operations

Secure Buffer Utilities

bool tls_bytes_compare(const void *buf1, const void *buf2, size_t len)

Constant-time comparison of two buffers. Always examines all len bytes regardless of where a difference is found, so the result does not leak information about the position of the mismatch through timing.

Use this instead of memcmp() whenever comparing secrets — MACs, digests, or decrypted values.

Parameters:
  • buf1 – First buffer.

  • buf2 – Second buffer.

  • len – Number of bytes to compare.

Returns:

true if the buffers are identical, false otherwise.

void tls_secure_memzero(void *ptr, size_t len)

Zero len bytes at ptr in a way the compiler cannot optimize away. Uses a volatile write loop so that zeroing of sensitive material (keys, plaintexts, passwords) is guaranteed to reach memory even when the buffer is not used afterwards.

Call this before freeing or reusing any stack buffer that held key material or plaintext.

Parameters:
  • ptr – Buffer to zero.

  • len – Number of bytes to zero.


Common Cryptography Usages

The examples below cover common patterns. They are not exhaustive, but demonstrate the idiomatic way to combine the APIs above.

File Integrity

Hash a file’s contents at two points in time and compare the digests to detect tampering or corruption. tls_bytes_compare() is used instead of memcmp() to avoid timing leaks.

#include <fileioc.h>
#include <lwip.h>
#include <cryptography.h>

if(!lwip_start()) return 1;

uint8_t digest_initial[TLS_SHA256_DIGEST_LEN];
uint8_t digest_second[TLS_SHA256_DIGEST_LEN];
struct tls_hash_context h;

/* Hash on first read */
if (tls_hash_context_init(&h, TLS_HASH_SHA256)) {
    uint8_t f = ti_Open("lwIP", "r");
    if (f) {
        size_t f_len = ti_GetSize(f);
        uint8_t *fp = ti_GetDataPtr(f);
        tls_hash_update(&h, fp, f_len);
        tls_hash_digest(&h, digest_initial);
        ti_Close(f);
    }
}

/* ... intervening activity ... */

/* Hash again and compare */
if (tls_hash_context_init(&h, TLS_HASH_SHA256)) {
    uint8_t f = ti_Open("lwIP", "r");
    if (f) {
        size_t f_len = ti_GetSize(f);
        uint8_t *fp = ti_GetDataPtr(f);
        tls_hash_update(&h, fp, f_len);
        tls_hash_digest(&h, digest_second);
        ti_Close(f);
        if (!tls_bytes_compare(digest_initial, digest_second, TLS_SHA256_DIGEST_LEN))
            printf("file contents have changed\n");
    }
}

Encrypt a File at Rest

This example uses the key API to encrypt a complete file with AES-256-GCM. The application supplies the password and a nonzero PBKDF2 iteration count chosen for its latency and password-protection requirements. Both examples assume lwip_start() has succeeded.

The salt is authenticated as AAD. The stored layout is [16-byte salt][encrypt blob] where the blob is the self-describing buffer returned by tls_cipher_encrypt_aad() — it contains the IV, ciphertext, and GCM tag in one contiguous allocation. The application must retain the same iteration count for decryption; this minimal format does not store it.

#include <fileioc.h>
#include <cryptography.h>
#include <string.h>

bool save_encrypted(const char *name, const char *password, size_t rounds,
                    const uint8_t *plaintext, size_t len)
{
    if (!name || !password || !plaintext || !len || !rounds)
        return false;
    uint8_t secret[32], salt[16];
    uint8_t *blob = NULL;
    bool ok = false;
    uint8_t file = 0;
    struct tls_key key = {
        .type = TLS_KEY_TYPE_AES,
        .aes = { .len = sizeof(secret), .data = secret },
    };

    tls_random_bytes(salt, sizeof(salt));
    if (!tls_pbkdf2(password, strlen(password), salt, sizeof(salt),
                    secret, sizeof(secret), rounds, TLS_HASH_SHA256))
        goto cleanup;
    /* Encrypt; IV is generated internally. Salt is the AAD. */
    blob = tls_cipher_encrypt_aad(&key, TLS_ALG_AES_256_GCM,
                                  salt, sizeof(salt), plaintext, len);
    if (!blob) goto cleanup;

    /* blob layout: <u16 iv_len><iv><u16 ct_len><ciphertext><u16 tag_len><tag> */
    file = ti_Open(name, "w");
    if (!file) goto cleanup;
    /* Determine blob size: 3 × uint16_t headers + IV + ciphertext + tag. */
    size_t blob_len = 6 + TLS_KEY_GCM_IV_LEN + len + TLS_KEY_AES_TAG_LEN;
    ok = ti_Write(salt, 1, sizeof(salt), file) == sizeof(salt) &&
         ti_Write(blob, 1, blob_len, file) == blob_len;
cleanup:
    if (file) ti_Close(file);
    tls_cipher_blob_free(blob);
    tls_secure_memzero(secret, sizeof(secret));
    return ok;
}

Decrypt a File at Rest

Read the stored salt and encrypt blob, then derive the same key and decrypt. tls_cipher_decrypt_aad() verifies the GCM tag before decrypting and returns NULL on any mismatch or error. The plaintext blob it returns carries a uint16_t length prefix followed by the plaintext bytes. The headers from the encryption example are also required here.

bool load_encrypted(const char *name, const char *password, size_t rounds,
                    uint8_t *plaintext, size_t capacity, size_t *written)
{
    if (!written) return false;
    *written = 0;
    if (!name || !password || !rounds || !plaintext) return false;
    uint8_t file = ti_Open(name, "r");
    if (!file) return false;
    uint8_t secret[32], salt[16];
    size_t size = ti_GetSize(file);
    /* File layout: [16-byte salt][blob]; blob minimum is 6 bytes of headers. */
    size_t salt_len = sizeof(salt);
    bool ok = false;
    uint8_t *enc_blob = NULL, *dec_blob = NULL;
    if (size <= salt_len + 6) goto cleanup;
    size_t blob_len = size - salt_len;
    enc_blob = malloc(blob_len);
    if (!enc_blob) goto cleanup;
    if (ti_Read(salt,     1, salt_len,  file) != salt_len ||
        ti_Read(enc_blob, 1, blob_len,  file) != blob_len)
        goto cleanup;
    if (!tls_pbkdf2(password, strlen(password), salt, salt_len,
                    secret, sizeof(secret), rounds, TLS_HASH_SHA256))
        goto cleanup;
    struct tls_key key = {
        .type = TLS_KEY_TYPE_AES,
        .aes = { .len = sizeof(secret), .data = secret },
    };
    /* salt is the AAD; tag is verified internally before any decryption. */
    dec_blob = tls_cipher_decrypt_aad(&key, TLS_ALG_AES_256_GCM,
                                      salt, salt_len, enc_blob);
    if (!dec_blob) goto cleanup;
    /* dec_blob layout: <uint16_t plain_len><plaintext> */
    size_t plain_len = (size_t)(dec_blob[0] | ((uint16_t)dec_blob[1] << 8));
    if (plain_len > capacity) goto cleanup;
    memcpy(plaintext, dec_blob + 2, plain_len);
    *written = plain_len;
    ok = true;
cleanup:
    tls_cipher_blob_free(dec_blob);
    free(enc_blob);
    ti_Close(file);
    tls_secure_memzero(secret, sizeof(secret));
    return ok;
}