ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ct_eq.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ct_eq.h
6 * @brief Constant-time comparison for secret-dependent checks.
7 *
8 * One implementation, so every MAC, tag, digest and signature check in the library compares the same
9 * way. A second copy is a second chance to write the early-out version by accident, and an early-out
10 * compare is a timing oracle no test catches.
11 *
12 * Zeroing storage is not here: mmgr_zero_buf() is a memory-manager operation and lives in
13 * locus_carcerum/locus_carcerum.h, beside the cellblock that zeroes on release.
14 *
15 * The compare is `static inline` here, so it inlines into the caller's loop. ::CtEq is the same
16 * compare reached through the namespace, with the operands on the handle.
17 *
18 * @author Douglas Quigg (dstroy0)
19 * @date 2026
20 */
21
22#ifndef PROTOCORE_CT_EQ_H
23#define PROTOCORE_CT_EQ_H
24
25#include "protocore_config.h" // the entry point: the enable gate below, and the widths
26
27#if PROTOCORE_ENABLE_CT_EQ
28
29#include "memoria_operor/memoria_operor.h"
30
32
33/**
34 * @brief Constant-time equality of two @p n-byte buffers: returns true iff every byte matches, in time
35 * independent of where (or whether) they first differ.
36 *
37 * Use this for every secret-dependent comparison - AEAD tags, MACs, digests, signature check values - so a
38 * timing side channel cannot reveal how many leading bytes matched. Never use memor.cmp() for those (it returns
39 * early on the first mismatch). The XOR-accumulate has no data-dependent branch; only the final all-zero test
40 * (the intended result) is a comparison.
41 */
42static inline proto_bool protocore_ct_eq(const void *a, const void *b, size_t n)
43{
44 const uint8_t *pa = (const uint8_t *)a;
45 const uint8_t *pb = (const uint8_t *)b;
46 uint8_t diff = 0;
47 for (size_t i = 0; i < n; i++)
48 {
49 diff |= (uint8_t)(pa[i] ^ pb[i]);
50 }
51 return diff == 0;
52}
53
54/** @brief The two buffers a compare runs over. */
55typedef struct
56{
57 const void *a; ///< the first buffer
58 const void *b; ///< the second, holding n readable bytes like the first
59 size_t n; ///< how many bytes are compared
60} CtEqEqArgs;
61
62/**
63 * @brief Constant-time equality.
64 *
65 * A caller sets the members the call takes, invokes it through ::CtEq, and reads the outcome off the
66 * same handle.
67 *
68 * CtEq.eq_args.a = computed_tag;
69 * CtEq.eq_args.b = received_tag;
70 * CtEq.eq_args.n = 16;
71 * CtEq.eq(work);
72 * if (CtEq.equal) { ... }
73 *
74 * @var CtEqNs::eq_args the two buffers a compare runs over
75 * @var CtEqNs::ok a call's true/false outcome; false on a null pointer
76 * @var CtEqNs::equal true when all n bytes matched; false on a null pointer
77 * @var CtEqNs::eq compare the two buffers in time independent of where they first differ
78 *
79 * @ref CtEqNs::equal carries the answer and @ref CtEqNs::ok carries whether the call ran, so a null
80 * pointer reads as not equal rather than as a match.
81 *
82 * @c work goes unread: the compare runs over the caller's two buffers and needs none of its own, so
83 * this module takes no borrow, holds no context, and reaches the same answer whatever is passed.
84 *
85 * No storage member and no context: a caller sets operands and reads @ref CtEqNs::equal, and that is
86 * all the surface there is.
87 */
88typedef struct
89{
90 CtEqEqArgs eq_args;
91 proto_bool ok;
92 proto_bool equal;
93} CtEqVars;
94
95/** @brief The operands and the outcome. */
96extern CtEqVars CtEqV;
97
98/** @brief The entries. */
99typedef struct
100{
101 void (*const eq)(uint8_t *work);
102} CtEqNs;
103
104// What the table binds, defined once in the .c and taking one parameter each: everything
105// else an entry needs is an operand in CtEqV or a region of the borrow at a fixed offset.
106void protocore_ct_eq_eq(uint8_t *work);
107
108// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
109// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
110// `CtEq.eq(work)` resolves to a named function and becomes a DIRECT call. An extern table
111// leaves the call indirect and the symbol live at every level, -O2 -flto included.
112static const CtEqNs CtEq __attribute__((unused)) = {
113 .eq = protocore_ct_eq_eq,
114};
115
117
118#endif // PROTOCORE_ENABLE_CT_EQ
119
120#endif // PROTOCORE_CT_EQ_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97