A fixed-size byte-array comparison helper with source-level
constant-time discipline. First real-crypto candidate
per the explicit slot reserved by crypto_verify's toy-MAC
graduation framing.
This is the smallest real-crypto artifact that forces the
exact distinction Concrete should be good at: the code is
simple, but the claim is subtle. Six-line OR-accumulate
over [u8; 16] arrays; security depends on properties the
code surface alone cannot demonstrate (no early exit, loop
count independent of contents, branch-free fold). Concrete's
role is to make those properties statically visible and to
prove the equality reasoning that backs them.
Two functions:
ct_compare(a: [u8; 16], b: [u8; 16]) -> i32— returns 1 iffaandbare byte-equal at every position, 0 otherwise. Always loops 16 times; no early exit; uses OR-accumulate over byte-level XORs; branch happens once at the end on the accumulator.main(entry point withConsoleforprintln) — runs 6 in-source tests and reports pass/fail.
nix develop --command bash -c '.lake/build/bin/concrete \
examples/constant_time_tag/src/main.con -o /tmp/ct && /tmp/ct'Runs 6 hand-written cases (zeros, all-ones, single-byte diff
at position 0, single-byte diff at position 15, all-bytes-
differ, 0xFF tags). Output ends with All 6 tests passed.
and exits 0.
Stated up front so the rest of the README does not oversell.
Not HMAC, not Ed25519, not a substitute for vetted crypto
libraries. This is a constant-time comparison helper, not a
MAC or signature. Do not deploy ct_compare as your auth-tag
check; use a vetted constant-time-comparison primitive from
a maintained cryptographic library. Audience here is
reviewers evaluating Concrete's proof workflow on
crypto-adjacent code.
Not a constant-time proof at the machine level. The
source has no early exit and a fixed loop bound — necessary
for constant-time execution. It is not sufficient: LLVM is
allowed to introduce branches during optimization; target
CPUs leak through cache, branch predictor, speculative
execution. Concrete does not have target-level timing
analysis today. assumptions.toml's
[claims.machine_level_constant_time] entry names this
explicitly as assumed_not_proved.
Three Lean theorems, checked by the Lean kernel at make build:
Concrete.Proof.ct_compare_equal_zeros_correct
ct_compare on two all-zero tags returns 1
Concrete.Proof.ct_compare_same_tag_correct
for any 16-byte tag t (parametric in all 16 byte values),
ct_compare(t, t) = 1
Concrete.Proof.bitxor_u8_self_zero
helper: any u8 value xor'd with itself is 0
--report proof-status reports ct_compare as proved,
backed by the universal same-tag theorem.
The same-tag theorem is the substantive crypto-adjacent
claim: equal tags always pass, with all 16 loop iterations
executed, regardless of byte values. The proof closes by
a single simp invocation that chains BitVec.xor_self
(per-iteration: b ^ b = 0) and BitVec.zero_or (per-
iteration: 0 | 0 = 0) through 16 unrolled iterations of
the bounded while loop. No induction over array contents is
required because the invariant — diff stays at 0 — is
uniform.
ct_compare fits the ProvableV1 profile (see
docs/PROVABLE_V1.md). The
function uses only constructs in the supported surface: u8
BitVec bitxor and bitor, array indexing, a bounded
16-iteration while_ loop, and an i32 accumulator branch
at the end. The registered theorem's coverage is
one_direction — universal positive direction over arbitrary
byte values, with the iff direction named (not hidden) as
open follow-up work. main is outside ProvableV1 by
design (entry point with Console). The machine-level
constant-time gap is a target-level claim outside the
ProvableV1 source / PExpr scope; assumptions.toml's
[claims.machine_level_constant_time] carries it.
Stated up front:
- The negative direction. The theorem proves
ct_compare a a = 1. It does NOT provect_compare a b = 1 ⟹ a = b(the iff). Closing the reverse direction needs an inductive argument over byte positions: "if any byte differs, the OR-accumulator is nonzero after the loop, so the final branch returns 0." This is the natural stretch theorem; helpers (bitxor_u8_self_zero,bitor_u8_zero_zero,ct_loop_iteration_invariant) are landed for the work. The oracle (see below) closes this gap empirically. - Source-level structural constant-time as a Concrete
guarantee. No
constant_timeprofile exists yet to name "loop count must be independent of input contents" as a checkable rule. Today the discipline is enforced by manual review of the source's structural properties (noreturninside the loop, fixed bound, no content-dependent branch). Future Phase 3 work would lift this to a static profile.CATCHES.mdnames this gap explicitly. - Machine-level constant time. See above; Phase 3 / target-model work.
- No allocation.
--report allocis empty. Both tag arrays are passed by value as[u8; 16];diffis au8local; the return isi32. No heap. - No trusted shells. Stricter than
fixed_capacity;Concrete.toml [policy].no_trusted = true. Tag arrays are constructed via array literals — no raw byte writes required. - No FFI, no externs.
[policy].no_externs = true. - Predictable profile.
--check predictablepasses forct_compare;mainis appropriately excluded (entry point withConsole). - Bounded loop. The single loop runs exactly 16 iterations (compile-time constant bound).
- Stack budget honored.
--report stack-depthreports max 209 bytes, within the policy budget of 256.
| Gate | Asserts |
|---|---|
make test-policy |
Concrete.toml [policy] — 8 enforced fields, stricter than other flagships |
make test-assumptions |
assumptions.toml matches reports, including the three [claims.*] entries |
make test-catches |
catches/01_alloc_in_compare_core.con rejects with E0520 requires Alloc |
make test-snapshots |
16 report snapshots byte-identical to baseline |
make test-ct-oracle |
Python reference + 600 seeded cases agree (3 seeds × 200) |
make test-verify-gates |
Pass-by-pass compiler gates green |
See assumptions.toml. Summary:
- hosted-libc host;
- u8 byte arithmetic modeled at u8 width via
BitVecround-trip onbitxorandbitor, with unsigned result interpretation (Int.ofNat ∘ toNat) so high-bit values (0x80..0xFF) surface as positiveInt; - no heap, no externs, no trusted, no FFI;
Consolecapability granted (used bymainforprintln);- machine-level timing properties are NOT proved (the
load-bearing assumption named in
[claims.machine_level_constant_time]).
Three layers, same as the prior three flagships:
- Lean kernel checks the three theorems above. Trust
anchor:
Lean kernel + Concrete.Proof.*. - Concrete compiler extracts properties and reports
them. Trusted, not yet Lean-verified (Phase 12).
The u8 PBinOp extensions (commits
12a4c94,28b1f2a) are the load-bearing additions for this candidate; their Phase 12 preservation obligations are named indocs/PROOF_OBLIGATIONS_REGISTER.mdR-17 and R-21. - Toolchain + runtime. LLVM, clang, linker, libc.
Not verified.
assumptions.tomlrecords the host/ toolchain exercised AND the machine-level-timing gap.
catches/01_alloc_in_compare_core.con is the same
ct_compare shape with an audit_byte helper that needs
with(Alloc) called inside the loop. Concrete refuses to
compile it with E0520 — requires Alloc but caller has (none).
The accepted example proves the language can express the
bounded-allocation discipline; the rejected companion
proves the language refuses the violation. See
CATCHES.md, which ALSO documents the timing/early-exit
gap that Concrete cannot mechanically catch today.
The Lean theorem covers the positive direction universally (equal tags → return 1, for any byte values). It does NOT yet cover the negative direction (differing tags → return 0). The oracle closes that gap empirically:
- 200 cases per seed × 3 seeds = 600 differential cases.
- ~152 of 200 cases per seed differ in at least one byte; every single one returns 0 against the Python reference.
- Cases cover per-position single-byte diffs (positions 0, 15, and random middle), multiple-byte diffs, all-byte diffs, and high-bit byte values.
This is the right shape for an honest proof + evidence story: the theorem is universal where it can be cheap; the oracle catches what the theorem does not yet cover.
10 of 10 bars met as of HEAD; graduated 2026-05-30. See
AUDIT.md for the per-bar state. The substantive
proof-side claim is ct_compare_same_tag_correct; the
substantive evidence claim is the oracle's 600 cases
covering both directions; the substantive
discipline-violation claim is the alloc-in-compare-core
catch.
AUDIT.md— graduation bars and progress.CATCHES.md— negative-pair narrative + named gap.assumptions.toml— declared trust surface.Concrete.toml[policy]— enforced budgets.oracle/reference.py+oracle/run_oracle.sh— the differential harness.src/proof-registry.json+Concrete/Proof.lean— attached theorems.docs/PROOF_OBLIGATIONS_REGISTER.mdR-17, R-21 — the Phase 12 preservation obligations this candidate's u8 PBinOp extensions created.examples/parse_validate/,examples/crypto_verify/,examples/fixed_capacity/— the three sibling graduated flagships; this README's structural template.