Security & Cryptography

SafeC provides a security library in std/crypto/ with symmetric encryption (AES-GCM, ChaCha20-Poly1305), hashing, HMAC, HKDF, random number generation, key exchange (X25519, P-256/P-384 ECDH), signatures (RSA PKCS#1/PSS, ECDSA), X.509 parsing with signature verification, and TLS record-layer framing. Complete TLS connections are std/tls.

Each algorithm's state is a struct: you get one from a static constructor such as AesCtx::new_128 or Sha256Ctx::new and call its methods. The hash contexts share the Hasher / BlockHash traits, so HMAC and one-shot hashing are written once over every hash.

c
#include <std/crypto/crypto.h>  // master header: pulls in all modules

WARNING

SafeC's crypto modules are checked against published test vectors, but have had no independent audit. The public-key code (X25519, P-256/P-384, RSA private-key operations), ChaCha20-Poly1305 and every tag comparison run in constant time; AES is table-based, so its timing can depend on the key through the CPU cache — prefer ChaCha20-Poly1305 where that matters. Use with that caveat in security-critical production contexts.


AES

c
#include <std/crypto/aes.h>

AES-128 and AES-256 block cipher with ECB, CBC and GCM modes. Full S-box, key expansion, and MixColumns implementation.

Constant Value
AES_BLOCK_SIZE 16 bytes
AES128_KEY_SIZE / AES256_KEY_SIZE 16 / 32 bytes
AES128_ROUNDS / AES256_ROUNDS 10 / 14
AES_KS_WORDS 60 (key-schedule words)

API

c
struct AesCtx {
    unsigned int  ks[60];             // expanded key schedule
    int           rounds;             // 10 (AES-128) or 14 (AES-256)
    unsigned char iv[16];             // CBC initialisation vector

    // Constructors (IV zeroed)
    static struct AesCtx new_128([]const unsigned char key);   // key: 16 bytes
    static struct AesCtx new_256([]const unsigned char key);   // key: 32 bytes

    // ECB — encrypt/decrypt one 16-byte block in-place
    void encrypt_block([]unsigned char block);
    void decrypt_block([]unsigned char block);

    // CBC — `len` must be a multiple of 16; updates self.iv in-place
    void cbc_encrypt([]unsigned char data);
    void cbc_decrypt([]unsigned char data);

    // Set the CBC IV
    void set_iv([]const unsigned char iv);

    // GCM (NIST SP 800-38D), 12-byte IV, 16-byte tag. Encrypts data in place
    // and writes the tag over aad[0..aad_len) and the ciphertext; aad may be
    // null when aad_len is 0. Never reuse an IV with the same key.
    void gcm_encrypt([]const unsigned char iv, []const unsigned char aad, []unsigned char data, []unsigned char tag);
    // Checks the tag in constant time; only if it matches, decrypts in place.
    // 1 if authentic, 0 if not (data is left as the ciphertext).
    int gcm_decrypt([]const unsigned char iv, []const unsigned char aad, []unsigned char data, []const unsigned char tag);

    // Wipe the key schedule and IV (volatile stores) when done.
    void destroy() consume;
}

INFO

cbc_encrypt / cbc_decrypt encrypt the buffer in-place and update self.iv after each block, ready for chaining the next call. The pointer arithmetic over the buffer happens inside unsafe {}.

Example

c
#include <std/crypto/aes.h>

int main() {
    unsigned char key[16];
    unsigned char iv[16];
    unsigned char buf[32];
    const &char text = "Hello, SafeC! AES CBC test data.";   // 32 bytes
    unsafe {
        int i = 0;
        while (i < 16) { key[i] = (unsigned char)(i * 17); iv[i] = 0; i = i + 1; }
        i = 0;
        while (i < 32) { buf[i] = (unsigned char)text[i]; i = i + 1; }
    }

    struct AesCtx ctx = std::AesCtx::new_128(key[..]);
    ctx.set_iv(iv[..]);
    ctx.cbc_encrypt(buf[0..32UL]);   // buf is now ciphertext

    ctx.set_iv(iv[..]);               // reset the IV and decrypt back
    ctx.cbc_decrypt(buf[0..32UL]);   // buf holds the plaintext again
    ctx.destroy();                    // wipe the key schedule
    unsafe { return buf[0] == (unsigned char)'H' ? 0 : 1; }
}

SHA-256

c
#include <std/crypto/sha256.h>

SHA-256 and SHA-224 with streaming (update) and one-shot interfaces. Sha256Ctx is a BlockHash.

c
struct Sha256Ctx {
    unsigned int  h[8];           // current hash state
    unsigned char buf[64];        // partial block buffer
    unsigned long total_bytes;    // total bytes processed
    int           is224;          // 1 = SHA-224 variant

    static struct Sha256Ctx new();       // SHA-256
    static struct Sha256Ctx new_224();   // SHA-224

    // Feed bytes into the hash (streaming)
    void update([]const unsigned char data);

    // Finalise and write digest: 32 bytes for SHA-256, 28 for SHA-224
    void finish([]unsigned char digest);

    unsigned long digest_size() const;   // 32 or 28
    unsigned long block_size() const;    // SHA256_BLOCK_SIZE
}

// One-shot convenience
void sha256([]const unsigned char data, []unsigned char digest);

#define SHA256_DIGEST_SIZE  32   // bytes
#define SHA224_DIGEST_SIZE  28
#define SHA256_BLOCK_SIZE   64

Example

c
#include <std/crypto/sha256.h>

int main() {
    unsigned char a[32];
    unsigned char b[32];
    unsigned char msg[5];
    unsafe { msg[0] = 'h'; msg[1] = 'e'; msg[2] = 'l'; msg[3] = 'l'; msg[4] = 'o'; }

    std::sha256(msg[0..5UL], a[..]);              // one-shot

    struct Sha256Ctx ctx = std::Sha256Ctx::new();  // streaming, two chunks
    ctx.update(msg[0..3UL]);
    unsafe { ctx.update(msg[3..5UL]); }
    ctx.finish(b[..]);                             // same digest as the one-shot

    int same = 1;
    unsafe { int i = 0; while (i < 32) { if (a[i] != b[i]) same = 0; i = i + 1; } }
    return same ? 0 : 1;
}

SHA-512 / SHA-384

c
#include <std/crypto/sha512.h>

Same interface as SHA-256, with 64-bit words and a 128-byte block. Sha512Ctx is a BlockHash.

c
#define SHA512_DIGEST_SIZE  64   // bytes
#define SHA384_DIGEST_SIZE  48
#define SHA512_BLOCK_SIZE   128

struct Sha512Ctx {
    unsigned long long h[8];
    unsigned char      buf[SHA512_BLOCK_SIZE];
    unsigned long long total_bytes;   // 64-bit count; the high half of the 128-bit length is written as 0
    int                is384;         // 1 = SHA-384

    static struct Sha512Ctx new();       // SHA-512
    static struct Sha512Ctx new_384();   // SHA-384

    void update([]const unsigned char data);
    void finish([]unsigned char digest);  // 64 bytes (SHA-512) or 48 (SHA-384)
    unsigned long digest_size() const;   // 64 or 48
    unsigned long block_size() const;    // SHA512_BLOCK_SIZE
};

void sha512([]const unsigned char data, []unsigned char digest);
void sha384([]const unsigned char data, []unsigned char digest);

SHA-1

c
#include <std/crypto/sha1.h>

Broken hash

SHA-1 has practical collision attacks. It is here only because some protocols require it — the WebSocket handshake's Sec-WebSocket-Accept is defined with SHA-1. Use SHA-256 or SHA-512 for anything else.

c
#define SHA1_DIGEST_SIZE  20   // bytes
#define SHA1_BLOCK_SIZE   64

struct Sha1Ctx {
    unsigned int  h[5];
    unsigned char buf[SHA1_BLOCK_SIZE];
    unsigned long total_bytes;

    static struct Sha1Ctx new();

    void update([]const unsigned char data);
    void finish([]unsigned char digest);   // 20 bytes
    unsigned long digest_size() const;    // SHA1_DIGEST_SIZE
    unsigned long block_size() const;     // SHA1_BLOCK_SIZE
};

void sha1([]const unsigned char data, []unsigned char digest);

Hash traits

c
#include <std/crypto/hash.h>   // crypto.h includes it

Every hash context streams the same way: update feeds bytes, finish writes the digest. hash.h names that shape as two traits, so code that works over any hash is written once. Each call is resolved at compile time; there is no dispatch at run time.

Trait Requires Satisfied by
Hasher update(data), finish(digest) (byte slices; the context is spent after, and finish panics on a slice shorter than the digest), digest_size() const Sha1Ctx, Sha256Ctx (SHA-256 or SHA-224), Sha512Ctx (SHA-512 or SHA-384), Hmac<H>
BlockHash Hasher + block_size() const: the compression-function block, which HMAC pads its key to Sha1Ctx, Sha256Ctx, Sha512Ctx
Generic code Bound Does
hash_bytes(h, data, digest) Hasher the digest of data with a fresh context h, taken by value: hash_bytes(Sha512Ctx::new_384(), msg, d[..])
Hmac<H> BlockHash HMAC over that hash

A type of your own is a Hasher once it has the three methods; nothing else declares it. Below, one generic function prints the digest for two std hashes and for a non-cryptographic FNV-1a checksum. FNV-1a has no block_size, so it is a Hasher but not a BlockHash, and Hmac<Fnv1a> is rejected where it is named.

c
#include <std/crypto/crypto.h>
#include <std/str.h>
#include <std/io.h>

// The digest of a string, in hex, with any Hasher.
generic<H: Hasher>
void print_digest(H h, const &char msg) {
    unsigned char d[64];
    std::hash_bytes(h, std::str_bytes(msg), d[..]);
    const &char hex = "0123456789abcdef";
    unsigned long i = 0UL;
    while (i < h.digest_size()) {
        unsafe { std::print_char((int)hex[d[i] >> 4]); std::print_char((int)hex[d[i] & 15]); }
        i = i + 1UL;
    }
    std::println("");
}

// FNV-1a: not a cryptographic hash, but it has the three methods,
// so it is a Hasher (not a BlockHash: it has no block_size).
struct Fnv1a {
    unsigned int h;
    static struct Fnv1a new();
    void update([]const unsigned char data);
    void finish([]unsigned char digest);
    unsigned long digest_size() const;
};
struct Fnv1a Fnv1a::new() { struct Fnv1a f; f.h = 2166136261U; return f; }
void Fnv1a::update([]const unsigned char data) {
    unsigned long i = 0UL;
    while (i < data.len) {
        self.h = (self.h ^ (unsigned int)data[i]) * 16777619U;
        i = i + 1UL;
    }
}
void Fnv1a::finish([]unsigned char digest) {   // bounds-checked: digest must hold 4 bytes
    digest[0] = (unsigned char)(self.h >> 24); digest[1] = (unsigned char)(self.h >> 16);
    digest[2] = (unsigned char)(self.h >> 8);  digest[3] = (unsigned char)self.h;
}
unsigned long Fnv1a::digest_size() const { return 4UL; }

int main() {
    print_digest(std::Sha256Ctx::new(), "abc");       // 32 bytes
    print_digest(std::Sha512Ctx::new_384(), "abc");   // 48 bytes
    print_digest(Fnv1a::new(), "abc");                 // 4 bytes
    return 0;
}
text
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7
1a47e90b

HMAC

c
#include <std/crypto/hmac.h>

HMAC (RFC 2104) written once over any BlockHash: HMAC-SHA1, -SHA224, -SHA256, -SHA384 and -SHA512 are the same code with a different hash context. JWT signing uses HMAC-SHA256.

c
#define HMAC_SHA256_SIZE 32   // bytes

generic<H: BlockHash>
struct Hmac {
    H inner;   // H((key ^ ipad) || message)
    H outer;   // H((key ^ opad) || inner digest), fed by finish()

    // Keyed by key[0..keyLen) over `fresh`, a new context of the hash to use
    // (Sha256Ctx::new(), Sha512Ctx::new_384(), ...). A key longer than the
    // hash's block is hashed first (RFC 2104).
    static struct Hmac<H> new(H fresh, []const unsigned char key);
    void update([]const unsigned char data);
    void finish([]unsigned char mac);   // digest_size() bytes; spent after
    unsigned long digest_size() const;          // the hash's digest size
};

// One-shot HMAC-SHA256: the 32-byte MAC of data[0..dataLen) under key[0..keyLen).
void hmac_sha256([]const unsigned char key, []const unsigned char data, []unsigned char out);

Hmac<H> streams like a hash and is a Hasher itself, so hash_bytes and any generic<H: Hasher> code take it. The type argument is inferred from the target, as in struct Hmac<Sha256Ctx> mac = Hmac::new(Sha256Ctx::new(), ...). The implementation is checked against the RFC 2202 and RFC 4231 test vectors for every std hash.

c
#include <std/crypto/hmac.h>
#include <std/crypto/sha256.h>
#include <std/crypto/sha512.h>

int main() {
    unsigned char key[3] = { 'k', 'e', 'y' };
    unsigned char msg[5] = { 'h', 'e', 'l', 'l', 'o' };

    unsigned char a[HMAC_SHA256_SIZE];
    std::hmac_sha256(key[0..3UL], msg[0..5UL], a[..]);          // one-shot

    struct Hmac<Sha256Ctx> mac = std::Hmac::new(std::Sha256Ctx::new(), key[0..3UL]);
    mac.update(msg[0..2UL]);                                       // streaming
    unsafe { mac.update(msg[2..5UL]); }
    unsigned char b[HMAC_SHA256_SIZE];
    mac.finish(b[..]);                                              // same as a

    unsigned char tag[SHA512_DIGEST_SIZE];                          // HMAC-SHA512
    struct Hmac<Sha512Ctx> mac512 = std::Hmac::new(std::Sha512Ctx::new(), key[0..3UL]);
    mac512.update(msg[0..5UL]);
    mac512.finish(tag[..]);

    int same = 1;
    unsafe { int i = 0; while (i < 32) { if (a[i] != b[i]) same = 0; i = i + 1; } }
    return same ? 0 : 1;
}

RNG — ChaCha20 CSPRNG

c
#include <std/crypto/rng.h>

A cryptographically secure pseudo-random number generator based on ChaCha20. Output is produced in RNG_BLOCK_SIZE (64-byte) ChaCha20 blocks.

c
struct RngCtx {
    // Seed in place from the OS's random source (BCryptGenRandom on Windows,
    // getentropy on macOS, getrandom on Linux), else rdrand (x86-64), else
    // /dev/urandom. Returns 1; aborts with a diagnostic if none works (e.g.
    // bare metal): use init_seed with your own entropy source there.
    int           init();

    // Deterministic seed in place (tests, reproducible simulations).
    void          init_seed(const unsigned char seed[32]);

    void fill([]unsigned char out);   // len random bytes
    unsigned int  rand32();
    unsigned long long rand64();
    unsigned long rand_range(unsigned long bound);   // uniform in [0, bound), no modulo bias
};

Example

c
#include <std/crypto/rng.h>
#include <std/io.h>

int main() {
    struct RngCtx rng;
    rng.init();   // aborts if there is no entropy source

    std::print("dice: ");
    std::println_int((long long)rng.rand_range(6UL) + 1LL);

    unsigned char key[32];
    rng.fill(key[0..32UL]);   // a random 256-bit key
    return 0;
}

HKDF

c
#include <std/crypto/hkdf.h>

HKDF (RFC 5869) over any BlockHash, through Hmac<H> — HKDF-SHA256 and HKDF-SHA384 are TLS 1.3's key schedule. Both functions take a fresh hash context by value to name the hash.

c
generic<H: BlockHash>
generic<H: BlockHash> void hkdf_extract(H fresh, []const unsigned char salt, []const unsigned char ikm, []unsigned char prk);   // digest_size() bytes
generic<H: BlockHash>
generic<H: BlockHash> int hkdf_expand(H fresh, []const unsigned char prk, []const unsigned char info, []unsigned char out);    // 0 if out_len > 255 * digest_size()

A null or empty salt is digest_size() zero bytes, as the RFC specifies.

c
unsigned char prk[SHA256_DIGEST_SIZE];
unsigned char okm[42];
std::hkdf_extract(std::Sha256Ctx::new(), salt[0..13UL], ikm[0..22UL], prk[..]);
std::hkdf_expand(std::Sha256Ctx::new(), prk[0..32UL], info[0..10UL], okm[0..42UL]);

ChaCha20-Poly1305

c
#include <std/crypto/chacha20.h>

The ChaCha20 stream cipher, the Poly1305 authenticator and their AEAD composition (RFC 8439) — TLS_CHACHA20_POLY1305_SHA256's record protection. All three run in constant time: no table lookups and no branches on key, nonce, message or tag bytes; chacha20_poly1305_open compares every tag byte.

c
#define CHACHA20_KEY_SIZE    32
#define CHACHA20_NONCE_SIZE  12
#define POLY1305_KEY_SIZE    32
#define POLY1305_TAG_SIZE    16

void chacha20_xor([]const unsigned char key, []const unsigned char nonce, unsigned int counter, []unsigned char data);          // encrypt = decrypt
void chacha20_block([]const unsigned char key, []const unsigned char nonce, unsigned int counter, []unsigned char out);                            // one 64-byte keystream block

struct Poly1305 {                                 // streaming, one-time key (r || s)
    static struct Poly1305 new([]const unsigned char key);
    void update([]const unsigned char data);
    void pad16();                                 // zero-pad the input so far to 16 bytes
    void finish([]unsigned char tag);              // 16 bytes; wipes the state
};
void poly1305([]const unsigned char key, []const unsigned char msg, []unsigned char tag);

// AEAD: encrypts data in place and writes the 16-byte tag.
void chacha20_poly1305_seal([]const unsigned char key, []const unsigned char nonce, []const unsigned char aad, []unsigned char data, []unsigned char tag);
// 1 and decrypted in place if authentic; 0 (data untouched) if not.
int chacha20_poly1305_open([]const unsigned char key, []const unsigned char nonce, []const unsigned char aad, []unsigned char data, []const unsigned char tag);

Never reuse a nonce with the same key.


X25519

c
#include <std/crypto/x25519.h>

Curve25519 Diffie-Hellman (RFC 7748) on 32-byte little-endian keys: a fixed 255-step Montgomery ladder with branch-free swaps over constant-time field arithmetic.

c
#define X25519_KEY_SIZE 32

// out = scalar * point (the scalar is clamped). 0 when the result is all
// zeros — a low-order peer key, which must be rejected — else 1.
int  x25519([]unsigned char out, []const unsigned char scalar, []const unsigned char point);   // 32-byte slices
// out = scalar * 9: the public key.
void x25519_public([]unsigned char out, []const unsigned char scalar);
c
unsigned char priv[32]; unsigned char pub[32]; unsigned char shared[32];
rng.fill(priv[0..32UL]);
std::x25519_public(pub[..], priv[..]);
if (!std::x25519(shared[..], priv[..], peer[..])) { /* reject */ }

P-256 and P-384: ECDH and ECDSA

c
#include <std/crypto/ec.h>

The NIST curves secp256r1 and secp384r1, named by their TLS group ids (EC_P256 = 23, EC_P384 = 24). Points use projective coordinates with the complete addition formulas of Renes–Costello–Batina, so no input — the point at infinity, doubling, a hostile point — takes another code path; scalar multiplication is a fixed-length double-and-add-always ladder with branch-free selection over constant-time Montgomery arithmetic, which makes key generation, ECDH and signing constant time. Peer keys are checked to be on the curve. Signing derives its nonce deterministically (RFC 6979). Keys and coordinates are big-endian; public keys are uncompressed SEC1 points (0x04 || X || Y, 65 or 97 bytes).

c
#define EC_MAX_SCALAR 48     // P-384 scalar / coordinate
#define EC_MAX_POINT  97     // an uncompressed P-384 point
#define EC_MAX_SIG    104    // a DER P-384 signature

unsigned long ec_scalar_size(int curve);    // 32 / 48, 0 for another id
unsigned long ec_point_size(int curve);     // 65 / 97
int ec_keygen(int curve, &RngCtx rng, []unsigned char priv, []unsigned char pub);
int ec_public_key(int curve, []const unsigned char priv, []unsigned char pub);   // 0: priv not in [1, n-1]
int ec_point_valid(int curve, []const unsigned char pub);
int ecdh_shared(int curve, []const unsigned char priv, []const unsigned char peer, []unsigned char out);        // the x coordinate; 0 for an invalid peer point
int ecdsa_verify(int curve, []const unsigned char pub, []const unsigned char hash, []const unsigned char r, []const unsigned char s);
int ecdsa_verify_der(int curve, []const unsigned char pub, []const unsigned char hash, []const unsigned char sig);
int ecdsa_sign_der(int curve, []const unsigned char priv, []const unsigned char hash, []unsigned char sig, &unsigned long sig_len);

struct EcPrivateKey {          // from SEC1 "EC PRIVATE KEY" or PKCS#8 DER
    int           curve;
    unsigned char d[EC_MAX_SCALAR];
    unsigned char pub[EC_MAX_POINT];
    unsigned long pub_len;
    int init_der([]const unsigned char der);
    void destroy();
};

The hash is truncated to the curve's size as FIPS 186-4 specifies.


RSA

c
#include <std/crypto/rsa.h>

RSA signatures (RFC 8017) for 1024–4096-bit moduli: verification with PKCS#1 v1.5 and PSS — what certificate chains and TLS use — and signing for TLS servers. Signing exponentiates in constant time (no CRT) and checks the result with the public key before returning it. Hash ids are DIGEST_*.

c
struct RsaPublicKey {
    // From a DER RSAPublicKey (the BIT STRING of an rsaEncryption SubjectPublicKeyInfo).
    int init_der([]const unsigned char der);
    // ...
};
int rsa_verify_pkcs1(const &RsaPublicKey key, int alg, []const unsigned char hash, []const unsigned char sig);
int rsa_verify_pss(const &RsaPublicKey key, int alg, []const unsigned char hash, []const unsigned char sig, unsigned long salt_len);

struct RsaPrivateKey {
    struct RsaPublicKey pub;
    int init_der([]const unsigned char der);   // PKCS#1 or PKCS#8
    void destroy();
};
int rsa_sign_pss(const &RsaPrivateKey key, int alg, []const unsigned char hash, &RngCtx rng, []unsigned char sig, &unsigned long sig_len);
int rsa_sign_pkcs1(const &RsaPrivateKey key, int alg, []const unsigned char hash, []unsigned char sig, &unsigned long sig_len);

The arithmetic underneath is std/crypto/bignum.h's BnMont: fixed-width Montgomery multiplication with a branch-free final subtraction, bn_mod_exp_public (for public exponents) and bn_mod_exp_secret (fixed 4-bit windows, table entries read by masked scans).


Digests by id and constant-time helpers

c
#include <std/crypto/digest.h>
#include <std/crypto/ct.h>

Signature schemes name their hash in data, so the verifiers take a DIGEST_* id (DIGEST_SHA1, _SHA224, _SHA256, _SHA384, _SHA512): digest(alg, data, out) returns the digest length (0 for an unknown id), DigestCtx::new(alg) streams, mgf1_xor is RFC 8017's mask generation function. ct.h has the comparisons and selections std's crypto uses on secrets: ct_eq, ct_is_zero, ct_copy_if, ct_eq_u32 — each looks at every byte, with no early exit.


SecureAllocator

c
#include <std/crypto/secure_alloc.h>

Wraps a slab allocator with zeroing-on-free, so sensitive data (keys, passwords) is erased when returned to the pool.

Fixed-size slots also mean no size metadata can leak.

c
struct SecureAllocator {
    struct SlabAllocator slab;

    // count slots of obj_size bytes each (heap-backed, SlabAllocator::new)
    static struct SecureAllocator new(unsigned long obj_size, unsigned long count);

    // One slot for size bytes aligned to align; null when every slot is in
    // use, size > obj_size, or align is above slab.max_align() (at most 8).
    ?&heap unsigned char alloc(unsigned long size, unsigned long align);
    void          dealloc(?&heap unsigned char ptr);  // zeroes the slot, then frees it; null is a no-op
    unsigned long available() const;
    void          destroy() consume;     // zeroes every slot, frees the backing buffer
};

SecureAllocator is an Allocator, so the typed alloc_t / dealloc_t work over it (and dealloc_t zeroes the slot too), as does any generic<A: Allocator> code:

c
#include <std/crypto/secure_alloc.h>

struct Key { unsigned char bytes[32]; };

int main() {
    struct SecureAllocator sa = std::SecureAllocator::new(sizeof(struct Key), 4UL);
    ?&heap Key k = std::alloc_t(&sa);   // null when full
    match (k) {
        case some(x): {
            x.bytes[0] = (unsigned char)7;
            std::dealloc_t(&sa, x);      // zeroed, then back on the free list
        }
        case null: { }
    }
    sa.destroy();                        // zeroes every slot, frees the buffer
    return 0;
}

X.509 Certificate Parser

c
#include <std/crypto/x509.h>

Parses X.509v3 DER-encoded certificates. Extracts subject/issuer distinguished names, validity period, Subject Alternative Names (SAN), BasicConstraints, and KeyUsage extensions.

Fixed limits: X509_MAX_NAME_LEN (128 bytes per name), X509_MAX_SAN (8 SAN entries), X509_MAX_SAN_LEN (64 bytes each), X509_KEY_LEN (256-byte public key).

Structs

c
struct X509Validity {
    unsigned long long not_before;  // Unix timestamp (dates before 1970 clamp to 0)
    unsigned long long not_after;
}

struct X509Name {
    char cn[128];   // Common Name
    char o[128];    // Organisation
    char c[4];      // Country
}

struct X509San {
    char names[8][64];  // DNS SAN entries
    int  count;
}

struct X509Cert {
    X509Name       subject;
    X509Name       issuer;
    X509Validity   validity;
    X509San        san;
    unsigned char  pubkey[256];     // the SubjectPublicKeyInfo body (truncated to 256 bytes)
    unsigned long  pubkey_len;
    unsigned char  serial[20];
    unsigned long  serial_len;
    int            is_ca;           // BasicConstraints CA=TRUE
    int            key_usage;       // raw KeyUsage bits

    int is_valid_at(unsigned long long unix_time) const;
    int matches_hostname(const &char host) const;  // DNS SANs (CN only without SANs); wildcard *.domain
    int is_ca_cert() const;

    // Parse a DER-encoded certificate into this struct (zeroed first).
    // Returns 0 on success, negative on error.
    int init([]const unsigned char der);

    // 1 if issuer_cert's subject matches this certificate's issuer name.
    // Structural check only: see x509_verify_signed_by for signatures.
    int verify_chain(const &X509Cert issuer_cert) const;
}

Parsing and chain checks

A certificate is parsed in place: declare a struct X509Cert and call cert.init(der). cert.verify_chain(&ca) only compares names — X509Cert keeps no signature; verify signatures with the functions below.

Full parsing and signature verification

X509Parsed locates every field path validation needs inside a DER certificate you keep — spans are byte ranges of it — with no fixed limits on names or SAN entries; x509_verify_signed_by checks a signature with the issuer's key (RSA PKCS#1 v1.5 and PSS, ECDSA P-256/P-384; SHA-1 signatures are refused); x509_match_host is RFC 6125 host matching. std/tls builds and validates whole chains with them.

c
struct X509Span { unsigned long off; unsigned long len; };

struct X509Parsed {
    struct X509Span tbs, serial, issuer, subject, spki, key, sig, san, subject_cn;
    int  version;
    int  key_type;       // X509_KEY_RSA, X509_KEY_EC_P256, X509_KEY_EC_P384, X509_KEY_ED25519
    int  sig_type;       // X509_SIG_RSA_PKCS1, X509_SIG_RSA_PSS, X509_SIG_ECDSA, X509_SIG_ED25519
    int  sig_hash;       // DIGEST_*
    int  pss_salt;
    unsigned long long not_before, not_after;
    int  has_basic_constraints, is_ca, path_len;   // path_len -1: none
    int  has_key_usage, key_usage;                 // X509_KU_* bits
    int  has_eku, eku_server, eku_client, eku_any;
    int  has_name_constraints;
    int  unknown_critical;   // a critical extension this parser doesn't process
};

int x509_parse([]const unsigned char der, &X509Parsed out);          // 0 on success
int x509_verify_signed_by([]const unsigned char der, const &X509Parsed c, []const unsigned char issuer_der, const &X509Parsed issuer);  // 1 if valid
int x509_issued_by([]const unsigned char der, const &X509Parsed c, []const unsigned char issuer_der, const &X509Parsed issuer);         // issuer == subject
int x509_match_host([]const unsigned char der, const &X509Parsed c, const &char host);

x509_match_host compares DNS names case-insensitively against dNSName SANs (a *. wildcard matches exactly one leftmost label, and needs at least two labels after it), IPv4/IPv6 literals against iPAddress SANs, and falls back to the subject CN only when the certificate has no DNS or IP SAN.

Example

c
#include <std/crypto/x509.h>
#include <std/io.h>

void check_cert([]const unsigned char der, unsigned long long now) {
    struct X509Cert cert;

    if (cert.init(der) != 0) {
        std::println("parse error");
        return;
    }
    if (!cert.is_valid_at(now)) {
        std::println("certificate expired");
        return;
    }
    if (cert.matches_hostname("api.example.com")) {
        std::println("hostname: OK");
    }
    if (cert.is_ca_cert()) {
        std::println("is CA certificate");
    }
}

INFO

matches_hostname compares case-insensitively and supports wildcard certificates: *.example.com matches api.example.com but not a.b.example.com. The subject CN is checked only when the certificate has no DNS SAN entries (RFC 6125).


TLS 1.3 Record Layer

c
#include <std/crypto/tls.h>

Implements the TLS 1.3 record layer (RFC 8446 §5). Handles framing, encryption, and decryption of TLS records. Key scheduling and the handshake protocol are not included — supply keys after completing the handshake externally. For complete TLS connections — handshake, certificates, every TLS 1.3 cipher suite, the OS's TLS library where there is one — use std/tls.

Record protection is the AES-256-GCM AEAD of RFC 8446 §5.2 (the TLS_AES_256_GCM_SHA384 cipher suite's record layer): given the same traffic keys, protected records are byte-for-byte what other TLS 1.3 stacks produce. The test suite checks a record against an independent AES-GCM implementation.

Constants

c
#define TLS_CHANGE_CIPHER_SPEC  20
#define TLS_ALERT               21
#define TLS_HANDSHAKE           22
#define TLS_APPLICATION_DATA    23

// Alert levels and descriptions (build_alert)
#define TLS_ALERT_WARNING        1
#define TLS_ALERT_FATAL          2
#define TLS_ALERT_CLOSE_NOTIFY   0
#define TLS_ALERT_HANDSHAKE_FAIL 40
#define TLS_ALERT_DECODE_ERROR   50

#define TLS_MAX_RECORD_LEN  16384   // max plaintext per record
#define TLS_KEY_LEN         32      // AES-256
#define TLS_IV_LEN          12      // per RFC 8446 §5.3
#define TLS_TAG_LEN         16      // GCM tag

Structs

c
struct TlsTranscript {
    struct Sha256Ctx hash;

    void update([]const unsigned char data);
    void finish([]unsigned char out);     // 32 bytes; a snapshot — the transcript stays usable
};

struct TlsSession {
    unsigned char      write_key[TLS_KEY_LEN];
    unsigned char      write_iv[TLS_IV_LEN];
    unsigned char      read_key[TLS_KEY_LEN];
    unsigned char      read_iv[TLS_IV_LEN];
    unsigned long long write_seq;
    unsigned long long read_seq;
    int                handshake_done;
    struct TlsTranscript transcript;

    // A fresh session: zero keys, seq = 0, handshake_done = 0.
    static struct TlsSession new();

    // Encode one record into out[0..out_cap). Pre-handshake: plaintext;
    // after install_keys: TLSInnerPlaintext (payload || content type) sealed
    // with AES-256-GCM, outer type application_data, 16-byte tag. Returns
    // bytes written, 0 on error; a record that doesn't fit out_cap doesn't
    // consume a sequence number.
    unsigned long encode_record(unsigned char content_type, []const unsigned char payload, []unsigned char out);

    // Decode one record from data[0..data_len); writes the plaintext to out
    // and the inner content type to *content_type_out. Protected records are
    // authenticated first: a forged or corrupted record returns 0 (as does
    // incomplete input) and doesn't advance read_seq.
    unsigned long decode_record([]const unsigned char data, &unsigned char content_type_out, []unsigned char out);

    unsigned long build_alert(unsigned char level, unsigned char desc, []unsigned char out);

    // Installs traffic keys, resets both sequence numbers and switches to
    // protected records (is_established() becomes 1).
    void install_keys(const unsigned char write_key[TLS_KEY_LEN],
                      const unsigned char write_iv[TLS_IV_LEN],
                      const unsigned char read_key[TLS_KEY_LEN],
                      const unsigned char read_iv[TLS_IV_LEN]);
    int  is_established() const;
};

Nonce derivation

Per RFC 8446 §5.3, the per-record nonce is:

text
nonce = write_iv XOR (zero-padded big-endian sequence number)

This is computed automatically inside encode_record / decode_record. The same derivation is exposed as a method if you need it directly:

c
void TlsSession::compute_nonce(unsigned char nonce[TLS_IV_LEN], int is_write) const;

Example

c
#include <std/crypto/tls.h>

int main() {
    struct TlsSession session = std::TlsSession::new();
    // ... after the handshake: session.install_keys(wk, wi, rk, ri);

    const &char msg = "GET / HTTP/1.1\r\n";
    unsigned char record[256];
    unsigned long n = 0UL;
    unsafe { n = session.encode_record(TLS_APPLICATION_DATA, ((const unsigned char*)msg)[0..16UL], record[0..256UL]); }
    // transmit record[0..n) over TCP ...

    unsigned char type = 0;
    unsigned char plain[256];
    unsigned long got = session.decode_record(record[0..n], &type, plain[0..256UL]);
    return (got == 16UL && type == (unsigned char)TLS_APPLICATION_DATA) ? 0 : 1;
}