Constant-time ChaCha20-Poly1305 AEAD in portable C99, with a timing side-channel validator. Zero dependencies, freestanding-friendly, RFC vectors byte-exact. Not a replacement for libsodium — a reference that is small enough to read end-to-end in an afternoon.
Status: v0.1.0 — ChaCha20 + Poly1305 + AEAD + timing validator. See CHANGELOG.md.
Constant-time crypto is the kind of code everyone depends on and almost nobody reads. Writing it from scratch is a sharpening exercise: every branch, every memory access, every compiler hint matters.
ctcrypt is a clean-room implementation of ChaCha20-Poly1305 (IETF
variant, RFC 8439) with:
- every hot-path operation reduced to
uint32_tarithmetic (add, xor, rotate-left); no table lookups indexed by secret bytes; no branches conditioned on secret data; - a dudect-style timing validator that measures per-block
rdtscpcycles across two key populations and runs Welch's two-sample t-test on the distributions; - fail-closed AEAD decryption that wipes the output buffer on tag mismatch;
- RFC 8439 test vectors covering every public primitive.
ctcrypt/
├── LICENSE
├── SECURITY.md
├── CHANGELOG.md
├── Makefile
├── include/
│ ├── chacha20.h # stream cipher API
│ ├── poly1305.h # MAC API (init/update/final + one-shot)
│ └── aead.h # seal / open
├── src/
│ ├── chacha20.c # constant-time reference
│ ├── poly1305.c # 5×26-bit limb implementation
│ └── aead.c # RFC 8439 §2.8 glue
└── tests/
├── Makefile
├── test_chacha20.c # §2.3.2 + §2.4.2 vectors
├── test_poly1305.c # §2.5.2 vector + verify semantics
├── test_aead.c # §2.8.2 vector + 4 tamper cases
└── timing.c # cycle-level timing validator
make test # RFC 8439 unit tests
make -C tests timing # build timing harness
tests/timing 100000 # run validator with 100k samples per population#include "aead.h"
uint8_t key[32] = {...};
uint8_t nonce[12] = {...};
uint8_t aad[] = {...};
uint8_t plaintext[128];
uint8_t ct[sizeof plaintext];
uint8_t tag[16];
aead_seal(ct, tag,
plaintext, sizeof plaintext,
aad, sizeof aad,
key, nonce);
uint8_t pt[sizeof plaintext];
int ok = aead_open(pt,
ct, sizeof ct, tag,
aad, sizeof aad,
key, nonce);
if (!ok) {
/* pt has been wiped; treat as authentication failure */
}ChaCha20 and Poly1305 are also usable standalone:
chacha20_ctx_t c;
chacha20_init(&c, key, nonce, /* counter = */ 1);
chacha20_xor(&c, in, out, len); /* in-place safe */
uint8_t tag[16];
poly1305_auth(tag, message, message_len, one_time_key);The harness records cycle counts for chacha20_block with two key
populations (all-zero vs all-ones), trims outliers, and runs Welch's
two-sample t-test.
ctcrypt timing validator — ChaCha20 block
samples per population: 50000 (interleaved)
population A (key = 0x00...):
mean 1023.27 cycles stddev 4.43 min 1020 max 1042
population B (key = 0xff...):
mean 1023.31 cycles stddev 4.51 min 1020 max 1044
Welch's two-sample t-statistic: -0.142
threshold: |t| < 5.0
VERDICT: PASS (distributions indistinguishable)
Caveats:
- Timing experiments are noisy. For reliable numbers run on a quiet machine with a fixed CPU frequency and process affinity pinned to an isolated core.
- CI runners do not satisfy these conditions; a PASS there is weak evidence. A FAIL is a red flag worth investigating.
- This is a sanity check, not formal verification. For certification
see
ct-verifor Jasmin.
- Sprint 1: ChaCha20 core + §2.3.2 / §2.4.2 vectors
- Sprint 2: Poly1305 + AEAD + §2.5.2 / §2.8.2 vectors
- Sprint 3: timing validator, CI, SECURITY.md, v0.1.0
- Sprint 4: SSSE3 assembly block function (~3x)
- Sprint 5: XChaCha20 (extended nonce), streaming AEAD
- RFC 8439 — ChaCha20 and Poly1305 for IETF Protocols.
- D. Bernstein, ChaCha, a variant of Salsa20 (2008).
- D. Bernstein, The Poly1305-AES message-authentication code (2005).
- O. Reparaz, J. Balasch, I. Verbauwhede, Dude, is my code constant time?, DATE 2017 — the dudect methodology.
MIT — see LICENSE.
Baurzhan Atynov — bauratynov@gmail.com