The Makefile targets and the build variables that choose what a packaged object carries.
make check runs the gate: build with warnings as errors, linters,
unit tests, end-to-end against OpenSSL and Go, the differential oracle,
and the fast proof tier.
Other targets:
-
make lib RAND=externpackages the library as one relocatable object (bin/chapulin.o) exporting exactly the eight public calls and one data symbol, the build recordch_build_info_tcp_blocking. The eight arech_connect,ch_read,ch_write,ch_writable_len,ch_close,ch_ticket_obfuscated_age,ch_alert_sentandch_alert_received. Every internal symbol is localized, andlib-checkfails if the export list ever changes. The calls are per build on three axes:RAND=drbgpackages the reference generator and exportsch_drbg_seedandch_rand_bytes, and a ca mode exportsch_pubkey_from_pemfor provisioning, so aTRUST=ca-rsa RAND=drbgobject exports eleven calls.RAND=sessionexports nothing more and imports noch_rand_bytes: each session names its own source inch_cfg.rand_bytesandch_cfg.rand_io, which growch_cfg, and with itch_tls, by two pointers, and every init call refuses a NULLrand_bytes(decision 77,docs/entropy.md). The build record,ch_pubkey_from_pem, a server'sch_srv_check, a client'sch_ticket_obfuscated_ageand the two alert calls carry the transport in their symbol names (ch_build_info_tcp_nonblocking,ch_srv_check_quic_nonblocking), and the headers map the names you call to them, so one image links an object of each of two transports (decisions 61, 72 and 75,docs/porting.md).TRUST=webpkiexports the eight calls and no provisioning call.EXPORTER=onaddsch_export, the exporter of RFC 9846 §7.5, and 32 bytes toch_tls; it is off by default, so the figures inperformance.mdare a build that exports nothing, and it refusesTRANSPORT=quic-nonblocking, whose object compiles notls.c(decision 43).KEYLOG=onhands each traffic secret to ach_keyloghook the image defines, for an NSS key log; it adds no export, it imports the hook, and it is refused for a client in a raw or ca trust mode (decision 44, INV-29).WIDEMUL=nativedefinesCH_NATIVE_WIDEMULin the object: the builder states that this part's widening multiply runs in constant time, and every widening product then uses the CPU's multiply instead of 16x16 pieces (ct.h). The default,WIDEMUL=decomposed, makes no claim about the part.WIDEMUL=runtimeholds both multiplies in one object, for a host whose threads or CPUs differ (decision 87). Your program setsch_cfg.widemulin every session's configuration: toCH_WIDEMUL_CONSTANT_TIMEwhen the multiply runs in constant time on the CPU and in the mode the session's thread runs in, which on arm64 means a core with FEAT_DIT and PSTATE.DIT set, and on x86-64 the DOITM policy of your operating system, and toCH_WIDEMUL_NOT_STATEDotherwise. Every init call refuses any other value, 0 included. chapulin sets no CPU mode and probes nothing. Each file built on the multiply is in the object twice, so it is larger, and the value refusesX25519=wide, whose build states the multiply's timing once.TX_RECORD=NsetsCH_TX_PT, the most plaintext one outgoing record carries, to N bytes, a decimal integer from 512 to 16384; the peer'srecord_size_limitcan still lower it. Empty, the default, leaves 512. A host that sends bulk data raises it to send fewer, larger records, andch_tlsgrows to hold one sealed record: in stompy'sTRUST=webpki TRANSPORT=tcp-nonblocking ROLE=bothobject it measures 17,280 bytes atTX_RECORD=16384against 3,280 at the default. It changes only what the object sends;cfg.buf_lenstill sets the records it receives. A QUIC object seals no TLS record, soTRANSPORT=quic-nonblockingrefuses it (decision 71).RANDis the one build variable with no default:extern,drbgorsession. Compose withTRUST=raw-ecdsa,TRUST=ca-rsaorTRUST=webpki, andKEX=pq; theTRUST=webpkiobject carries every verifier, which is why that value names no algorithm.X25519=widereplaces the default 16-limb X25519 field withx25519_wide.c's five 51-bit limbs, for a 64-bit host:ct.hstops the build unless the compiler hasunsigned __int128and the build adds-DCH_NATIVE_MUL128toCFLAGS, its statement that the part's 64x64->128 multiply runs in constant time in the mode the part runs in (decision 52, INV-34). The Makefile never writes that define itself.CHACHA=vectorreplaceschacha20.c's one-block loop withchacha20_vector.c's eight blocks at a time on NEON or four on SSE2, for an arm64 or x86-64 host:chacha20_vector.hstops the build for any other target, and the build states nothing about timing (decision 82). WithWIDEMUL=nativeas well,poly1305_vector.cruns Poly1305 four blocks at a time on the vector widening multiply, whichCH_NATIVE_WIDEMULthen covers beside the scalar one (decision 83). It also carries every key exchange group it offers, X25519MLKEM768 and x25519 with a share each and secp256r1 listed after them for a HelloRetryRequest to ask for, somake TRUST=webpkirefuses aKEXvalue, which would select nothing there (decisions 53 and 63); setch_cfg.require_pqfor the hybrid alone.KEXchooses the group of a raw or ca client only, andROLE=serverandROLE=bothrefuse it too, because a server role holds all three groups in every build (decisions 54 and 63).SUITE=aesgcmaddsTLS_AES_128_GCM_SHA256andTLS_AES_256_GCM_SHA384to aTRUST=webpkiclient or a server role. On an arm64 or x86-64 host that object is a host object, below, whose sessions run AES-GCM on the AES instructions and the carry-less multiply where your program setsCH_CPU_CONSTANT_TIME_AES(decisions 50 and 89). A device object takesAES=extern: every AES block runs in ach_aes_block(key, key_len, in, out)the image defines, 16-byte and 32-byte keys both, and the build needs-DCH_AES_EXTERN_CONSTANT_TIME, the statement that the peripheral behind it runs in constant time (decision 68). The Makefile never writes that statement,ct.hrefuses the suite on theAES=softtable, and nothing in this tree can check a peripheral. A host session with the AES bit offers and prefersTLS_AES_256_GCM_SHA384, thenTLS_AES_128_GCM_SHA256, then ChaCha20, a host session without it offers ChaCha20 alone, and theAES=externbuild keeps ChaCha20 first (decision 80).AESis a device object's variable alone:soft, the default, orextern. The Makefile andbuild.zigrefuse anAESvalue for a host object, and the valuesAES=hwandAES=runtime, which chose the instructions when the object was built, are gone (decision 89). -
On an arm64 or x86-64 host,
TRUST=webpki,ROLE=serverandROLE=bothbuild a host object (decision 89). The Makefile andbuild.zigrun the host test on the compiler: it targets arm64 or x86-64, NEON or SSE2 on a little-endian core, and hasunsigned __int128. Where it passes, the object compiles with-DCH_CPU_RUNTIME, whichmake print-lib-defprints with the rest of its defines, andch_cfgholdscpu, your description of the CPU (cpu_cfg.h). Your program probes the CPU and sets the bits it found in every session's configuration:CH_CPU_PROBEDalways, which says you wrote the field,CH_CPU_CONSTANT_TIME_AESandCH_CPU_CONSTANT_TIME_MULTIPLYwhere you state those instructions run in constant time on that CPU in the mode your thread runs in, and on x86-64CH_CPU_AVX2andCH_CPU_VAES. Every init call andch_srv_checkrefuse a value withoutCH_CPU_PROBED, and one with a bit this object does not define for its architecture, such asCH_CPU_AVX2on arm64. chapulin probes nothing and sets no CPU mode. The host object holds the AES instructions, and in a QUIC object the software AES beside them, andCH_CPU_CONSTANT_TIME_AESpicks: with it the session runs the AES-GCM suites and QUIC's Initial packets on the instructions, and without it the session holds ChaCha20 alone, runs its Initial packets on the software AES, and init refuses a suite list that names an AES-GCM suite. No path reads the other bits yet: theCHACHA,WIDEMULandX25519variables still choose what each object runs, until decision 89's later commits move each choice to its bit. A raw or ca client builds the portable object on every target, and so does every product for any other target, so the defaultmake libhas nocpufield. To package a server's portable object on a host, set the host test's result empty on the command line,HOST_TARGET=. -
ch_buildis the object's build record (build.h): the axes it was compiled with, the sizes ofch_cfg,ch_tls,ch_ticket,ch_record,ch_quicandch_rsa_priv, and the bounds a program sizes its buffers from. A program that links the object compiles the headers under defines it writes itself, and a define it forgets changes those sizes while the program still links. So callch_build_matches(&ch_build)once at startup and stop when it returns 0; a program in another language compares the same fields with theCH_BUILD_macros.ch_buildis a macro for the transport's record,ch_build_info_tcp_blocking,ch_build_info_tcp_nonblockingorch_build_info_quic_nonblocking, and a Zig program names that record itself. No library call reads the record, and decisions 56 and 61 say what it holds, what it leaves out and why its name carries the transport. -
A Zig project (Zig 0.16.0) depends on chapulin as a package and gets the object
make libbuilds and a Zig API over it. The options are the Makefile's variables, with the same names and values, and the two hardware statements the Makefile takes inCFLAGSare options that default off.TX_RECORDtakes its number,.TX_RECORD = 16384:const chapulin = b.dependency("chapulin", .{ .target = target, .RAND = .@"extern", .TRANSPORT = .@"quic-nonblocking", .ROLE = .both, .TRUST = .webpki, .SUITE = .aesgcm, }); module.addImport("chapulin", chapulin.module("chapulin"));
The module
chapulinis the Zig API, and it carries the object, so the program adds noaddObjectFileof its own; a second one defines every public name twice. The program writesconst chapulin = @import("chapulin");, callschapulin.buildMatches()once, and runs sessions throughchapulin.recordorchapulin.quic(zig.md). Under.RAND = .sessioneach session's values carry astd.Random, and the program defines noch_rand_bytes.chapulin.cis the public headers, translated by translate-c under the defines the object compiled with, so its types have the object's layout and the program names no define. An image that links objects of two transports takes two dependencies and imports each one's module under a name of its own. The named lazy pathchapulin.ois the object andincludethe header directory, for a program that compiles the headers as C.build.zigcompiles every source into one relocatable object, andtools/localize_symbols.zigmakes every symbol but the public API local, asobjcopy -Gandnmedit -sdo for make, so one image links objects of two transports.make lint-zig-buildbuilds eight configurations both ways and requires the same sources, defines, exports and build record, and builds and runs Zig programs against each module (decisions 69, 70 and 73, INV-36). -
make checkskips a lint, a Wycheproof leg or a packaged-object leg that passed before on the same inputs, andmake -j checkruns its lints, legs and test runs side by side.tools/stamp.pystates what a skip keys on: the bytes of the files the check reads, the tools' versions, and the make and environment variables it runs under, never a time (INV-37).CHECK_NO_STAMPS=1runs every check. CI starts each job withoutbin/, so CI runs every check in full. -
make checkbuilds, and does not run, each program a bench or platform script compiles from a source list of its own:test/script-builds.shrunsbench/aead.sh --build,bench/record.sh --build,bench/primitives.sh --buildandtest/qemu-m3.sh --build, and buildsbench/insn_driver.cfrom the Makefile'sINSN_SRCS. The lanes check does not run, such assan-check,cross-check,m3-checkandcoverage, compile each test from the variable its own rule reads (decision 88, INV-40). -
make prove-slowruns the slow-tier proofs, one per nightly job. The runner caches by content, so an incremental run re-proves only what changed (PROVE_NO_CACHE=1forces a full run). It uses kissat when installed, which reaches the same verdicts faster;PROVE_SOLVER=builtinandPROVE_SOLVER=smt2pick the other back ends. -
make timingruns the constant-time check. Run it on an idle machine. -
make fuzzsmoke-runs the libFuzzer harnesses. -
make cxx-checkbuildschapulin.hpp, an optional header-only C++ wrapper. It forwards to the same C calls with no runtime cost, is freestanding, and compiles under-fno-exceptions -fno-rtti. It gives you a session that wipes its keys when it leaves scope. -
make hooks, once after clone, enables the commit-msg hook.
See CLAUDE.md for the house rules.