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
lwip/cryptography/random.hThe 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()ortls_random_bytes()unlesslwip_start()has already been called (which initializes entropy automatically).- Returns:
trueon success,falseif 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:
trueif the RNG is healthy and safe to use,falseotherwise. Onfalse, retrytls_random_init_entropy()or abort.
-
void *tls_random_bytes(void *buffer, size_t len)
Fill
bufferwithlencryptographically random bytes, synchronously. The health check must pass before calling this.- Parameters:
buffer – Caller-supplied destination buffer.
len – Number of bytes to fill.
- Returns:
bufferon 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
lenrandom bytes intoout, either synchronously or via lwIP timer callbacks. Only one request can be active at a time — checktls_rng_is_busy()before calling. The caller must keepoutvalid until the request completes (i.e. untilcbis 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)).okistrueon success.arg – Opaque pointer passed through to
cb.blocking –
trueto gather entropy immediately in the caller (may block for a long time);falseto gather in chunks vialwip_service_events()and invokecbwhen done.
- Returns:
trueif the request started or completed successfully,falseon failure (busy, bad args, or health failure).
-
bool tls_rng_is_busy(void)
Returns
trueif atls_request_random_bytes()request is currently in progress. Use this to guard against starting a second request before the first completes.- Returns:
trueif busy,falseif 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
lwip/cryptography/hash.hA 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:
trueon success,falseif 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:
trueon success,falseon 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
lwip/cryptography/hmac.hHMAC 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:
trueon success,falseon 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
lwip/cryptography/hkdf.hHKDF 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
NULLand0for 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:
trueon success,falseon 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
NULLand0for none.info_len – Length of info.
okm – Output keying material buffer.
okm_len – Desired output length in bytes (max:
255 * hash_len).
- Returns:
trueon success,falseon 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
NULLand0for none.context_len – Length of context.
out – Output buffer.
out_len – Desired output length in bytes.
- Returns:
trueon success,falseon 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_lenbytes).
- Returns:
trueon success,falseon 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
lwip/cryptography/passwords.hPBKDF2 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:
trueon success,falseon failure.
uint8_t key[32];
tls_pbkdf2("password", 8,
salt, salt_len,
key, sizeof(key),
100, /* iteration count */
TLS_HASH_SHA256);
Key Operations
lwip/cryptography/key.hThe 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
rsaunion member. Compatible withTLS_ALG_RSA_PKCS1_SHA256,TLS_ALG_RSA_PSS_RSAE_SHA256, andTLS_ALG_RSA_OAEP_SHA256.
-
TLS_KEY_TYPE_EC_P256
EC P-256 key. Populate the
ecunion member. Compatible withTLS_ALG_ECDSA_SECP256R1_SHA256(currently unimplemented).
-
TLS_KEY_TYPE_AES
AES key. Populate the
aesunion member. Compatible with allTLS_ALG_AES_*ciphers; the key length (aes.len) must match the selected algorithm (16 bytes for_128_*, 32 bytes for_256_*).
-
TLS_KEY_TYPE_UNKNOWN
-
struct tls_key
Self-describing key. Populate
typeand exactly one union member.-
tls_key_type_t type
Key material type. Drives dispatch — operations check this against the requested
algand reject mismatches before touching key material. Must be set correctly before passing the key to any operation.
-
bool allocated
truewhen this struct was returned bytls_key_import()and must be freed withtls_key_free(). Leavefalsefor stack-allocated or externally-managed keys. Do not set this flag manually.
-
struct tls_rsa_key rsa
Valid when
type == TLS_KEY_TYPE_RSA. Containsmodulus,mod_len,exponent, andexp_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 || Yfrom 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;lenmust be 16 (128-bit) or 32 (256-bit) to match the selected cipher.
-
tls_key_type_t type
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
typefield 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
NULLon error.data – PEM text or DER bytes.
len – Input length in bytes.
format –
TLS_KEY_FORMAT_PEMorTLS_KEY_FORMAT_DER.password – Passphrase for an encrypted PKCS#8 key;
NULLfor unencrypted input.
- Returns:
TLS_KEY_IMPORT_OKon success. Failures includeTLS_KEY_IMPORT_INVALID_ARG,TLS_KEY_IMPORT_ALLOC_FAIL,TLS_KEY_IMPORT_PARSE_FAIL,TLS_KEY_IMPORT_BAD_ALG,TLS_KEY_IMPORT_UNSUPPORTED_ENC, andTLS_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
typeto the appropriateTLS_KEY_TYPE_*constant.For AES, set
aes.datato the key bytes andaes.lento 16 or 32, matching the_128_*or_256_*algorithm you intend to use.For RSA, set
rsa.modulusandrsa.exponentto unsigned big-endian byte arrays with lengths inrsa.mod_lenandrsa.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.datato the uncompressed public point andec.lento 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
inand 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;
algand 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,
NULLon any error (bad args, RNG failure, allocation failure, or cipher error). Free withtls_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-
NULLaador a non-zeroaad_lenwith those algorithms returnsNULL. Otherwise identical totls_cipher_encrypt().- Parameters:
aad – Additional authenticated data. Pass
NULLand0for none.aad_len – Length of AAD in bytes.
- Returns:
Allocated ciphertext blob on success,
NULLon error. Free withtls_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
NULLon mismatch. Use the samealgthat was passed to encrypt.blobneed not have come fromtls_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,
NULLon any error or tag mismatch. The plaintext is atblob + 2and its length is the little-endianuint16_tatblob[0..1]. Free withtls_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-NULLaador a non-zeroaad_lenwith those algorithms returnsNULL. Otherwise identical totls_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,
NULLon error or mismatch. Free withtls_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
NULLwith 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
ivin bytes.ct – Ciphertext bytes.
ct_len – Length of
ctin bytes.tag – Authentication tag bytes, or
NULL.tag_len – Length of
tagin bytes.
- Returns:
Allocated ciphertext blob on success,
NULLon allocation failure. Free withtls_cipher_blob_free().
Cipher |
Member |
Operation |
|---|---|---|
|
|
Authenticated encryption/decryption; 12-byte IV, 16-byte tag. |
|
|
Authenticated encryption/decryption; 13-byte nonce, 16-byte tag. |
|
|
Encryption/decryption; 16-byte IV, no authentication tag. |
|
|
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()andtls_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()).
-
TLS_KEY_OP_OK
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
contentusingkey.Hashes
contentwith SHA-256 internally; pass the raw message, not a pre-computed digest. Dispatches onalgafter checkingkey->type; returnsTLS_KEY_OP_INVALIDwhenkey->typedoes not matchalg.Only signing-range algorithms are accepted (
TLS_ALG_IS_SIGNING). Passing an encryption-rangealgreturnsTLS_KEY_OP_UNKNOWN.- Parameters:
content – Message bytes to verify.
content_len – Length of
contentin bytes.sig – Raw signature bytes.
sig_len – Length of
sigin bytes.key – Key holding the public material;
key->typemust matchalg.alg – Signature scheme to use (e.g.
TLS_ALG_RSA_PKCS1_SHA256).
- Returns:
TLS_KEY_OP_OKon valid signature;TLS_KEY_OP_INVALID,TLS_KEY_OP_UNSUPPORTED, orTLS_KEY_OP_UNKNOWNotherwise.
-
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
contentwithkey, writing the raw signature tosig_out.Hashes
contentwith SHA-256 internally; pass the raw message, not a pre-computed digest.sig_len_outreceives the number of bytes written on success. The caller must pre-allocatesig_outlarge enough to hold the output (RSA signatures arekey->rsa.mod_lenbytes).Note
Signing is currently unimplemented for all algorithms, including RSA. All calls return
TLS_KEY_OP_UNSUPPORTEDuntil private-key signing is added.- Parameters:
content – Message bytes to sign.
content_len – Length of
contentin 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_OKon success;TLS_KEY_OP_UNSUPPORTEDuntil signing is implemented;TLS_KEY_OP_INVALIDorTLS_KEY_OP_UNKNOWNon bad arguments.
Algorithm |
Member |
Operation |
|---|---|---|
|
|
Verify PKCS#1 v1.5 signatures with SHA-256. |
|
|
Verify PSS signatures with SHA-256, MGF1-SHA-256 and a 32-byte salt. |
|
|
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->allocatedbefore doing anything: iffalsethe function returns immediately, making it safe to call on stack-allocated or externally-managed keys. Whentrue, 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 thepubkeymember embedded inside astruct 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. Usetls_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(), ortls_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 withtls_key_free().Stack
struct tls_key→ calltls_secure_memzero()on borrowed buffers yourself; never pass totls_key_free().tls_cipher_encrypt()/tls_cipher_decrypt()/tls_cipher_blob_assemble()→ free withtls_cipher_blob_free().struct tls_x509_object→ free withtls_x509_object_free(); never free its embeddedpubkeyindependently.
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 withtls_x509_object_free().Parse-in-place via
tls_x509_parse_certificate(): populates a caller-allocatedstruct tls_x509_objectwith 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_handshakeistrue).
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
truewhen populated bytls_x509_parse_certificate()(pointers borrow the caller’s buffer;tls_x509_object_free()is a no-op).falsewhen allocated bytls_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
notBeforefield: raw value bytes and the ASN.1 tag (ASN1_UTCTIMEorASN1_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
notAfterfield: 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 Extensioninside the[3] EXPLICITextensions wrapper;NULL/ 0 when no extensions are present.
-
struct tls_key pubkey
Public key extracted from SubjectPublicKeyInfo.
pubkey.allocatedis alwaysfalse; never pass totls_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.
-
bool from_handshake
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 inputpem_databuffer can be released after this call returns.- Parameters:
pem_data – PEM text starting with
-----BEGIN CERTIFICATE-----.size – Length of
pem_datain bytes.
- Returns:
Allocated object on success,
NULLon parse or allocation failure. Free withtls_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 inoutreference bytes insidecert_der; the caller must keepcert_derlive for the entire lifetime ofout. Setsout->from_handshake = true.- Parameters:
cert_der – DER-encoded certificate bytes.
cert_len – Length of
cert_derin bytes.out – Caller-allocated object to receive parsed fields.
- Returns:
trueon success,falseon 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_handshakeistrue(parse-in-place objects borrow their DER; the caller owns that buffer). Safe to call withNULL.
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
hostnamewith ASCII case-insensitive comparison and single leading-label wildcard support (*.example.commatchesfoo.example.combut notfoo.bar.example.comorexample.comitself). 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
iPAddressSAN 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_cnfor CN fallback; may beNULL.subject_cn_len –
obj->subject_cn_len.hostname – NUL-terminated hostname the connection was made to.
- Returns:
trueif the certificate covershostname.
-
bool tls_x509_time_in_validity(const struct tls_x509_object *cert, uint32_t now_secs)
Check whether
now_secsfalls 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:
trueifnow_secsis 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_objectvalidity 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_UTCTIMEorASN1_GENERALIZEDTIME).out_secs – Receives the Unix timestamp on success.
- Returns:
trueon 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
contentinternally before verifying.Dispatches on
alg:TLS_ALG_RSA_PKCS1_SHA256— RSASSA-PKCS1-v1.5 SHA-256TLS_ALG_RSA_PSS_RSAE_SHA256— RSASSA-PSS SHA-256 (saltLen=32)TLS_ALG_ECDSA_SECP256R1_SHA256— returnsTLS_KEY_OP_UNSUPPORTEDEncryption-range or unknown
alg— returnsTLS_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->typemust matchalg.alg – Signature scheme to verify.
- Returns:
TLS_KEY_OP_OKon valid signature;TLS_KEY_OP_INVALID,TLS_KEY_OP_UNSUPPORTED, orTLS_KEY_OP_UNKNOWNotherwise.
-
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_OKon success;TLS_KEY_OP_INVALID,TLS_KEY_OP_UNSUPPORTED, orTLS_KEY_OP_UNKNOWNotherwise.
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
lenbytes 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:
trueif the buffers are identical,falseotherwise.
-
void tls_secure_memzero(void *ptr, size_t len)
Zero
lenbytes atptrin 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;
}