LITLS -- Lilush TLS Stack

Overview

This document defines the LITLS cryptographic and TLS stack (src/litls/).

LITLS is a minimal, TLS 1.3-only cryptographic and TLS stack built for Lilush. The goal was to assemble a secure, efficient, and self-contained TLS implementation from components compatible with the OWL license -- meaning only public domain and MIT licensed code.

The foundation is Daniel J. Bernstein's cryptographic designs: X25519 for key exchange, ChaCha20-Poly1305 as the preferred cipher, Ed25519 for signatures.

If the world ran on DJB crypto alone, LITLS would be a much smaller project. But RFC 8446 compliance and real-world certificate chains demand more: AES-GCM is a mandatory cipher suite, and virtually every CA issues ECDSA P-256 or RSA certificates. So LITLS pragmatically borrows constant-time AES, ECDSA, and RSA implementations from BearSSL (MIT licensed) to cover the gaps.

LITLS has no external library dependency. All cryptographic code ships in-tree under src/litls/, compiled directly into the binary. The TLS 1.3 state machine is hand-written, purpose-built for Lilush's non-blocking I/O model.

Security notice: LITLS has not undergone a third-party security audit.

While it has test coverage and is being actively hardened, use in security-sensitive production environments is at your own risk.

Contributions and responsible disclosure are welcome.

Design principles

Source layout

src/litls/
  litls.h                  -- Public C API (primitives + X.509 + PEM)
  litls_lua.c              -- Lua bindings (registered as litls.core)
  litls_sha1.c             -- SHA-1 (legacy, git metadata only)
  litls_sha256.c           -- SHA-256 (streaming + single-shot)
  litls_sha384.c           -- SHA-384 (truncated SHA-512)
  litls_sha512.c           -- SHA-512
  litls_hmac.c             -- HMAC-SHA256, HMAC-SHA384
  litls_hkdf.c             -- HKDF extract/expand (SHA-256, SHA-384)
  litls_chacha20.c         -- ChaCha20 stream cipher (RFC 8439)
  litls_poly1305.c         -- Poly1305 MAC
  litls_chacha20poly1305.c -- ChaCha20-Poly1305 AEAD
  litls_gcm.c              -- AES-128/256-GCM AEAD (wraps BearSSL)
  litls_aes.c              -- AES block cipher (wraps BearSSL)
  litls_x25519.c           -- X25519 ECDH (from TweetNaCl)
  litls_ed25519.c          -- Ed25519 sign/verify (from TweetNaCl)
  litls_ecdsa_p256.c       -- ECDSA P-256 keygen/sign/verify (wraps BearSSL)
  litls_ecdsa_p384.c       -- ECDSA P-384 verify only (wraps BearSSL)
  litls_rsa_verify.c       -- RSA PKCS#1 v1.5 + PSS verify (wraps BearSSL)
  litls_base64.c           -- Base64 / Base64url encode/decode
  litls_rng.c              -- CSPRNG + secure memcmp + secure zero
  litls_pem_encode.c       -- EC private key PEM encoding
  bearssl/                 -- Extracted BearSSL primitives (MIT)
    aes_ct_ctr.c           -- AES in counter mode, constant-time
    ghash_ctmul32.c        -- GHASH for GCM, constant-time
    ec_p256_m15.c          -- P-256 field arithmetic
    ecdsa_i15_*.c          -- ECDSA operations
    rsa_i15_*.c            -- RSA verification
    ...                    -- ~40 files total
  x509/                    -- Self-written X.509/PKI layer
    asn1.c / asn1.h        -- ASN.1/DER reader + back-patching writer
    parse.c                -- X.509 certificate parser
    verify.c               -- X.509 chain validator
    csr.c                  -- PKCS#10 CSR DER encoder
  tls13/                   -- TLS 1.3 state machine
    tls13.h                -- Internal TLS 1.3 API
    record.c               -- Record layer (encrypt/decrypt, padding)
    keys.c                 -- Key schedule and traffic secret derivation
    handshake.c            -- Handshake message parsing/building
    client.c               -- Client handshake path
    server.c               -- Server handshake path

Cryptographic primitives

Sourcing rationale

The "DJB-first" principle applies where it does not compromise interoperability: X25519 for key exchange and ChaCha20-Poly1305 as the preferred cipher suite. AES-GCM and ECDSA P-256 are included because RFC 8446 §9.1 mandates TLS_AES_128_GCM_SHA256 and because real-world certificate chains use ECDSA P-256 or RSA.

Primitive reference

PrimitiveC APISourceLicense
SHA-1litls_sha1()Self-written
SHA-256litls_sha256(), streaming ctxBrad Conte's crypto-algorithmsPublic domain
SHA-384litls_sha384(), streaming ctxSamePublic domain
SHA-512litls_sha512(), streaming ctxTweetNaCl (internal)Public domain
HMAC-SHA256litls_hmac_sha256()Self-written (RFC 2104)
HMAC-SHA384litls_hmac_sha384()Self-written
HKDF-SHA256litls_hkdf_extract_sha256(), _expand_sha256()Self-written (RFC 5869)
HKDF-SHA384litls_hkdf_extract_sha384(), _expand_sha384()Self-written
ChaCha20litls_chacha20_block(), _encrypt()DJB reference, IETF variantPublic domain
Poly1305litls_poly1305_auth(), streaming _init/_update/_final()DJB / Andrew MoonPublic domain
ChaCha20-Poly1305litls_chacha20_poly1305_encrypt/decrypt()Composition of abovePublic domain
AES-128-GCMlitls_aes128_gcm_encrypt/decrypt()BearSSL aes_ct_ctr + ghash_ctmul32MIT
AES-256-GCMlitls_aes256_gcm_encrypt/decrypt()BearSSL (same backend)MIT
X25519litls_x25519(), _base()TweetNaCl crypto_scalarmult_curve25519Public domain
Ed25519litls_ed25519_keypair(), _sign(), _verify()TweetNaCl crypto_sign_ed25519Public domain
ECDSA P-256litls_p256_keygen(), _ecdsa_sign(), _ecdsa_verify()BearSSL ec_p256_m15 + ecdsa_i15MIT
ECDSA P-384litls_p384_ecdsa_verify() (verify only)BearSSL ec_p384_m15MIT
RSA PKCS#1 v1.5litls_rsa_pkcs1_verify()BearSSL rsa_i15_pkcs1_vrfyMIT
RSA-PSS-SHA256litls_rsa_pss_sha256_verify()RFC 8017 + BearSSL RSAMIT
Base64litls_base64_encode/decode(), _url_*()Self-written (RFC 4648)
CSPRNGlitls_random_bytes()Linux getrandom(2)
Secure memcmplitls_secure_memcmp()Self-written
Secure zerolitls_secure_zero()explicit_bzero wrapper
ECDSA sig convertlitls_ecdsa_sig_der_to_raw(), _raw_to_der()Self-written

TweetNaCl

TweetNaCl (~700 lines, public domain) is imported for X25519 and Ed25519 only. TweetNaCl uses XSalsa20, not IETF ChaCha20 -- the two are different constructions. For TLS 1.3's TLS_CHACHA20_POLY1305_SHA256, the IETF ChaCha20 variant (RFC 8439, 96-bit nonce) is used separately.

TweetNaCl functions used:

All other TweetNaCl functions (secretbox, box, stream) are unused and compiled out.

BearSSL extracts

BearSSL is a TLS 1.2 library; only its low-level cryptographic components are extracted. BearSSL's higher-level components (x509_minimal.c, ASN.1 codecs, PEM handling, TLS engine) are not imported -- the X.509/PKI and TLS 1.3 layers are self-written.

Extracted components:

BearSSL is MIT licensed. Original copyright headers are preserved in all extracted files under src/litls/bearssl/. See CREDITS.

X.509 / PKI layer

The X.509/PKI layer is entirely self-written (~1700 lines of C, no heap allocation). BearSSL's br_x509_minimal_engine was evaluated but rejected due to vtable mismatch and dependency explosion.

Certificate parsing

litls_x509_parse(der, der_len, info) extracts from a DER-encoded X.509 certificate:

FieldTypeDescription
common_namechar[256]Subject CN
common_name_lensize_tLength of CN
not_before / not_afterint64_tValidity window (Unix timestamps)
sans[]struct {name[256], len}Subject Alternative Names (DNS), max 32
san_countintNumber of SANs parsed
key_typeintLITLS_KEY_* identifier
pubkeyuint8_t[512]Subject public key (raw bytes)
pubkey_lensize_tLength of public key
rsa_e / rsa_e_lenuint8_t[8] / size_tRSA public exponent (RSA keys only)
sig_algointLITLS_SIG_* identifier
signature / signature_lenconst uint8_t* / size_tRaw signature (points into input DER)
tbs / tbs_lenconst uint8_t* / size_tTBS region (points into input DER)

Key types: LITLS_KEY_ECDSA_P256 (1), LITLS_KEY_RSA (2), LITLS_KEY_ECDSA_P384 (3), LITLS_KEY_ED25519 (4).

Signature algorithms: LITLS_SIG_ECDSA_SHA256 (1), LITLS_SIG_ECDSA_SHA384 (2), LITLS_SIG_RSA_SHA256 (3), LITLS_SIG_RSA_SHA384 (4), LITLS_SIG_RSA_SHA512 (5), LITLS_SIG_ED25519 (6).

Chain validation

litls_x509_verify_chain() validates a certificate chain (leaf-first):

  1. Parse each certificate via litls_x509_parse().

  2. For each cert i, verify its signature against cert i+1's public key.

  3. For the final cert, try all trust anchors by signature verification (not CN matching -- handles cross-signed intermediates correctly).

  4. Check validity periods against a caller-provided timestamp (now=0 skips time validation).

Error codes:

CodeConstantMeaning
0LITLS_X509_OKChain is valid
-1LITLS_X509_ERR_PARSEFailed to parse a certificate
-2LITLS_X509_ERR_SIGNATURESignature verification failed
-3LITLS_X509_ERR_EXPIREDCertificate outside validity window
-4LITLS_X509_ERR_NO_ANCHORNo matching trust anchor found
-5LITLS_X509_ERR_CHAINChain structure error
-6LITLS_X509_ERR_CONSTRAINTBasicConstraints / KeyUsage / EKU / pathLen violation
-7LITLS_X509_ERR_HOSTNAMELeaf SAN/CN does not match expected hostname

Limits: max 8 certificates in chain, max 256 trust anchors.

litls_x509_load_anchors(pem_data, pem_len, anchors, max) loads trust anchors from a PEM CA bundle, parsing each certificate to extract its public key.

PEM handling

FunctionDescription
litls_pem_decode()Strip PEM armor, base64-decode to DER
litls_ec_privkey_to_pem()Encode P-256 private key to SEC1 PEM
litls_parse_private_key_pem()Parse EC PRIVATE KEY or PKCS#8 PEM (P-256 and Ed25519)

CSR generation

litls_csr_generate(params, der_out, der_cap) creates a DER-encoded PKCS#10 CSR using ECDSA P-256 + SHA-256, for ACME certificate issuance. The CSR structure is hand-encoded DER (one domain + SANs). Max output: 4096 bytes.

TLS 1.3 protocol

Cipher suites

SuiteCodePriorityHash
TLS_CHACHA20_POLY1305_SHA2560x13031st (preferred)SHA-256
TLS_AES_128_GCM_SHA2560x13012ndSHA-256
TLS_AES_256_GCM_SHA3840x13023rdSHA-384

All three are advertised in ClientHello. The server selects from the intersection.

Key exchange

Only X25519 (NamedGroup 0x001D) is offered in the key_share extension. If a server rejects X25519 with HelloRetryRequest, the HRR is honored only if it requests X25519 again (error otherwise). Cookie extension (up to 512 bytes) is supported for HRR flows.

No P-256 key exchange -- only P-256 certificate verification.

Signature algorithms

Advertised in ClientHello and accepted in CertificateVerify (RFC 8446 §4.2.3 forbids RSASSA-PKCS1-v1_5 for TLS 1.3 handshake signatures, so it is not offered):

AlgorithmCodeUsage
ecdsa_secp256r1_sha2560x0403P-256 server certs
rsa_pss_rsae_sha2560x0804RSA server certs (CertificateVerify)
ed255190x0807Ed25519 server certs

RSA PKCS#1 v1.5 with SHA-256/384/512 is still accepted as an X.509 chain signature algorithm in litls_x509_verify_chain -- it is only forbidden in TLS 1.3 CertificateVerify.

Client handshake

ClientHello
  - supported_versions: [TLS 1.3]
  - supported_groups: [x25519]
  - key_share: [x25519: ephemeral_public_key]
  - signature_algorithms: [ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256,
                            ed25519]
  - server_name: <SNI hostname>
  - cipher_suites: [0x1303, 0x1301, 0x1302]

  ServerHello
    - parse key_share (server X25519 public key)
    - derive shared secret via X25519(ephemeral_priv, server_pub)
    - derive handshake_secret via HKDF key schedule

  {EncryptedExtensions}

  {Certificate}
    - decode certificate chain
    - validate against trust anchors
    - extract server public key

  {CertificateVerify}
    - verify signature over transcript hash

  {Finished}
    - verify HMAC over transcript

ClientFinished
  - send client Finished

  derive application traffic secrets
  CONNECTED

Server handshake

  ClientHello
    - parse cipher suites (select best)
    - parse key_shares (expect x25519)
    - parse SNI, select cert/key
    - generate ephemeral X25519 keypair
    - derive shared secret, handshake_secret

ServerHello (cipher suite, server X25519 key_share)
{EncryptedExtensions}
{Certificate} (leaf + chain)
{CertificateVerify} (sign transcript with server private key)
{Finished} (HMAC over transcript)

  ClientFinished
    - verify client Finished

  derive application traffic secrets
  CONNECTED

Key schedule

RFC 8446 §7.1 key schedule, using HKDF-SHA256 (or SHA384 for AES-256-GCM):

early_secret      = HKDF-Extract(0, PSK=0)
derived_secret    = Derive-Secret(early_secret, "derived", "")
handshake_secret  = HKDF-Extract(derived_secret, DHE)
client_hs_traffic = Derive-Secret(handshake_secret, "c hs traffic", CH..SH)
server_hs_traffic = Derive-Secret(handshake_secret, "s hs traffic", CH..SH)
...
master_secret     = HKDF-Extract(Derive-Secret(handshake_secret, "derived", ""), 0)
client_ap_traffic = Derive-Secret(master_secret, "c ap traffic", CH..SF)
server_ap_traffic = Derive-Secret(master_secret, "s ap traffic", CH..SF)

AEAD keys and IVs are derived per §7.3:

key = HKDF-Expand-Label(traffic_secret, "key", "", key_len)
iv  = HKDF-Expand-Label(traffic_secret, "iv",  "", 12)

Record nonce = per-record sequence number XOR'd with IV (§5.3).

Post-handshake messages

Non-blocking I/O integration

The TLS state machine is fully non-blocking. litls_tls13_handshake_step() returns one of:

ReturnMeaning
LITLS_DONEHandshake complete
LITLS_WANT_READNeeds more data from socket
LITLS_WANT_WRITEHas data to send
negativeFatal error

LEV's C core (lev_core.c) drives the state machine via starttls_init() and starttls_step(). The Lua coroutine loop in lev.lua yields on WANT_READ/WANT_WRITE and resumes when epoll signals readiness. See LEV for the Lua-facing TLS API.

Lua API

litls.core module

The litls.core module exposes all LITLS primitives to Lua. Most application code should use the higher-level crypto module or LEV's TLS integration instead of calling litls.core directly.

Base64

FunctionArgumentsReturns
base64_encode(data)binary stringbase64 string
base64_decode(data)base64 stringbinary string
base64url_encode(data)binary stringbase64url string
base64url_decode(data)base64url stringbinary string

Random

FunctionArgumentsReturns
random_bytes(n)length (0-65536)binary string

Hashes

FunctionArgumentsReturns
sha1(data)binary string20-byte hash
sha256(data)binary string32-byte hash
sha384(data)binary string48-byte hash
sha512(data)binary string64-byte hash

HMAC

FunctionArgumentsReturns
hmac_sha256(key, msg)binary strings32-byte MAC
hmac_sha384(key, msg)binary strings48-byte MAC

HKDF (RFC 5869)

FunctionArgumentsReturns
hkdf_extract_sha256(salt, ikm)binary strings32-byte PRK
hkdf_expand_sha256(prk, info, len)PRK + info + output lengthbinary string
hkdf_extract_sha384(salt, ikm)binary strings48-byte PRK
hkdf_expand_sha384(prk, info, len)PRK + info + output lengthbinary string

ChaCha20 / Poly1305

FunctionArgumentsReturns
chacha20_block(key, nonce, counter)32B key, 12B nonce, integer64-byte keystream block
chacha20_encrypt(key, nonce, counter, plaintext)32B key, 12B nonce, integer, dataciphertext
poly1305_auth(key, msg)32B key, data16-byte tag

AEAD

FunctionArgumentsReturns
chacha20_poly1305_encrypt(key, nonce, aad, pt)32B key, 12B nonce, AAD, plaintextciphertext, 16B tag
chacha20_poly1305_decrypt(key, nonce, aad, ct, tag)32B key, 12B nonce, AAD, ciphertext, tagplaintext or nil
aes128_gcm_encrypt(key, nonce, aad, pt)16B key, 12B nonce, AAD, plaintextciphertext, 16B tag
aes128_gcm_decrypt(key, nonce, aad, ct, tag)16B key, 12B nonce, AAD, ciphertext, tagplaintext or nil
aes256_gcm_encrypt(key, nonce, aad, pt)32B key, 12B nonce, AAD, plaintextciphertext, 16B tag
aes256_gcm_decrypt(key, nonce, aad, ct, tag)32B key, 12B nonce, AAD, ciphertext, tagplaintext or nil

X25519

FunctionArgumentsReturns
x25519(private_key, peer_public)32B each32-byte shared secret, or nil
x25519_base(private_key)32B scalar32-byte public key

Ed25519

FunctionArgumentsReturns
ed25519_keypair(seed)32B seedpublic_key (32B), secret_key (64B)
ed25519_sign(msg, pk, sk)message, 32B pk, 64B sk64-byte signature
ed25519_verify(sig, msg, pk)64B sig, message, 32B pktrue or false

Ed25519 / X25519 key conversion

FunctionArgumentsReturns
ed25519_to_x25519_pk(pk)32B Ed25519 public key32-byte X25519 public key
ed25519_to_x25519_sk(seed)32B Ed25519 seed32-byte X25519 private key
openssh_parse_ed25519(pem)OpenSSH private key PEM stringseed (32B), public_key (32B) or nil, err

ECDSA P-256

FunctionArgumentsReturns
p256_keygen()noneprivate_key (32B), public_key (65B)
p256_ecdsa_sign(hash, private_key)hash digest, 32B keyDER-encoded signature
p256_ecdsa_verify(hash, public_key, sig)hash, 65B key, DER sigtrue or false

ECDSA P-384

FunctionArgumentsReturns
p384_ecdsa_verify(hash, public_key, sig)hash, public key, DER sigtrue or false

RSA verify

FunctionArgumentsReturns
rsa_pkcs1_verify(hash, hash_id, sig, n, e)hash, HASH_SHA* constant, sig, modulus, exponenttrue or false
rsa_pss_sha256_verify(hash, sig, n, e)SHA-256 hash, sig, modulus, exponenttrue or false

Hex helpers

FunctionArgumentsReturns
hex_encode(data)binary stringhex string
hex_decode(hex)hex stringbinary string

X.509 / PEM / CSR

FunctionArgumentsReturns
pem_decode(pem)PEM stringDER binary
x509_parse(der)DER certificatecert info table
x509_verify_chain(chain, ca_pem)array of DER certs, CA PEMtrue or error code
x509_verify_hostname(der, hostname)leaf DER, hostname stringtrue on match, nil, err otherwise
csr_generate(priv, pub, domain, sans)32B key, 65B pub, domain, SANs tableDER binary

TLS 1.3 key schedule (for testing)

FunctionArgumentsReturns
tls13_expand_label_256(secret, label, context, len)secret, label string, context, output lengthexpanded key material
tls13_derive_secret_256(secret, label, hash)secret, label string, transcript hashderived secret

Constants

NameValueDescription
HASH_SHA2564Hash identifier for RSA verify functions
HASH_SHA3845Hash identifier for RSA verify functions
HASH_SHA5126Hash identifier for RSA verify functions

Higher-level modules

crypto module (src/crypto/crypto.lua): High-level wrapper over litls.core (via the crypto.core C binding). Provides SHA-1, SHA-256, HMAC, Base64/Base64url, ECC P-256 key management (keygen, sign, verify, JWK save/load), Ed25519 (keygen, sign, verify), CSR generation, PEM encoding, and X.509 certificate parsing. Most application code should use crypto rather than litls.core directly.

LEV TLS integration (src/lev/): TLS connections are created via lev.connect() with a tls config table, or sock:starttls() for server-side upgrade. The TLS handshake runs non-blocking inside the LEV event loop. See LEV for the full TLS API.

To be done

Non-goals

Testing

All LITLS tests live in tests/litls/ and use the testimony framework. Every primitive is validated against official RFC test vectors.

Test fileCoverage
test_sha.luaSHA-256, SHA-384, SHA-512 -- FIPS 180-4 vectors
test_hmac.luaHMAC-SHA256, HMAC-SHA384 -- RFC 4231 vectors
test_hkdf.luaHKDF extract/expand -- RFC 5869 Appendix A vectors
test_chacha20.luaChaCha20 block + encrypt -- RFC 8439 §2.4.2 vectors
test_poly1305.luaPoly1305 MAC -- RFC 8439 §2.5.2 vectors
test_aead.luaChaCha20-Poly1305 + AES-GCM AEAD -- RFC 8439 + NIST vectors
test_x25519.luaX25519 key exchange -- RFC 7748 §6.1 vectors
test_ed25519.luaEd25519 sign/verify -- RFC 8032 vectors
test_ecdsa_p256.luaECDSA P-256 keygen/sign/verify
test_base64.luaBase64 + Base64url encode/decode -- RFC 4648 vectors
test_rsa_verify.luaRSA PKCS#1 v1.5 + RSA-PSS-SHA256 verify -- NIST vectors
test_x509.luaCertificate parsing, chain validation, CSR generation
test_tls13_keys.luaKey schedule, Expand-Label, Derive-Secret -- RFC 8448 vectors
test_tls13_record.luaRecord framing, AEAD encrypt/decrypt, nonce construction
test_tls13_client.luaLive TLS 1.3 client handshake, HTTPS, cert verification
test_tls13_server.luaServer-side handshake, SNI, bidirectional data exchange
test_crypto_module.luacrypto module API (base64, SHA, HMAC, ECC, Ed25519, CSR, PEM)

The TLS section in tests/lev/test_lev.lua serves as the integration regression suite.

Known limitations