Skip to main content

pakery_core/
ct.rs

1//! Constant-time verification helpers (ctgrind pattern).
2//!
3//! These helpers wire secret material into Valgrind memcheck's definedness
4//! tracking, following the classic ctgrind idea (as used by graviola, aws-lc,
5//! and NSS): memory holding secrets is marked *undefined*, so any
6//! secret-dependent branch or memory index in the compiled artifact surfaces
7//! as a memcheck error when the test harness runs under
8//! `valgrind --track-origins=yes --error-exitcode=99`.
9//!
10//! Without the private `__ctgrind` cargo feature every function here compiles
11//! to a no-op; with it, they emit Valgrind client requests via [`crabgrind`]
12//! (a safe API — compatible with `#![forbid(unsafe_code)]`). The feature is
13//! internal to pakery's CI (`ct.yml`) and must never be enabled in production
14//! builds — the double-underscore prefix marks it exempt from semver.
15//!
16//! # Marking policy
17//!
18//! Marked secret at their byte boundaries: passwords, scalar/private-key
19//! bytes, DH outputs, OPRF outputs, PRKs and derived keys, envelope contents.
20//!
21//! Declassified (each call site carries a justification comment):
22//! - public-key and key-share encodings before wire serialization — public
23//!   by protocol design;
24//! - MAC tags before wire output — public once sent;
25//! - ciphertext (e.g. OPAQUE `masked_response`) before wire output;
26//! - boolean accept/reject decisions after a `subtle::ct_eq` comparison
27//!   ([`declassify_choice`]) — the abort/continue outcome is public;
28//! - copies of secret-scalar encodings fed to `curve25519-dalek` /
29//!   `p256` deserializers ([`declassify`] on a temporary copy): their
30//!   canonicity checks branch on a `subtle::CtOption` discriminant, which
31//!   memcheck would flag inside the dependency. Canonicity of an honestly
32//!   generated key is public information, and constant-time verification of
33//!   dependency-internal primitives is out of scope here (tracked upstream;
34//!   see SECURITY_TESTING.md, "Constant-time verification"). Where a parse is laundered
35//!   this way, the *result* of the following group operation is re-marked
36//!   secret so taint stays end-to-end.
37
38/// Whether the helpers emit real Valgrind client requests.
39///
40/// True only when the `__ctgrind` feature is on **and** crabgrind was built
41/// against real Valgrind headers. Without headers (e.g. a `--all-features`
42/// build on macOS or a runner without Valgrind installed) crabgrind falls
43/// back to stub headers whose version is the sentinel `0xBEDABEDA`, and
44/// every client request would panic at runtime — so the helpers downgrade
45/// to no-ops instead. `ct.yml` asserts this returns true (via the harness's
46/// `ct_harness_is_armed` test) to rule out a silently disarmed run.
47#[inline(always)]
48#[must_use]
49pub fn is_active() -> bool {
50    #[cfg(feature = "__ctgrind")]
51    {
52        crabgrind::VALGRIND_VERSION.0 != 0xBEDA_BEDA
53    }
54    #[cfg(not(feature = "__ctgrind"))]
55    false
56}
57
58/// Mark `bytes` as secret: Valgrind memcheck treats them as undefined, so any
59/// branch or memory index computed from them is reported as an error.
60///
61/// No-op unless the `__ctgrind` feature is enabled (and, at runtime, the
62/// process runs under Valgrind).
63#[inline(always)]
64pub fn mark_secret(bytes: &[u8]) {
65    #[cfg(feature = "__ctgrind")]
66    if is_active() {
67        let _ = crabgrind::memcheck::mark_memory(
68            bytes.as_ptr().cast(),
69            bytes.len(),
70            crabgrind::memcheck::MemState::Undefined,
71        );
72    }
73    #[cfg(not(feature = "__ctgrind"))]
74    let _ = bytes;
75}
76
77/// Declassify `bytes`: mark them as defined (public) for Valgrind memcheck.
78///
79/// Use only at boundaries where the data is genuinely public (wire output,
80/// public-key encodings, MAC tags being sent) — every call site must carry a
81/// justification comment. No-op unless the `__ctgrind` feature is enabled.
82#[inline(always)]
83pub fn declassify(bytes: &[u8]) {
84    #[cfg(feature = "__ctgrind")]
85    if is_active() {
86        let _ = crabgrind::memcheck::mark_memory(
87            bytes.as_ptr().cast(),
88            bytes.len(),
89            crabgrind::memcheck::MemState::Defined,
90        );
91    }
92    #[cfg(not(feature = "__ctgrind"))]
93    let _ = bytes;
94}
95
96/// Declassify the boolean outcome of a constant-time comparison.
97///
98/// The accept/reject decision after a `ct_eq` (MAC verification,
99/// identity-point rejection, shared-secret equality) is public: the protocol
100/// visibly aborts or continues. Converting `Choice → bool` directly would
101/// branch on secret-derived data (memcheck flags both the caller's `if` and
102/// `subtle`'s internal `debug_assert!`), so the decision byte is copied to
103/// the stack, declassified, and only then read.
104#[inline(always)]
105pub fn declassify_choice(choice: subtle::Choice) -> bool {
106    let mut byte = [choice.unwrap_u8()];
107    declassify(&byte);
108    // In release builds LLVM may keep the pre-declassify value in a register
109    // (`declassify` only takes a shared reference, so the memory is assumed
110    // unmodified across the call) and branch on that still-tainted copy.
111    // Routing the array through an opaque identity forces the branch below
112    // to re-read the now-defined memory.
113    let byte = core::hint::black_box(&mut byte);
114    byte[0] != 0
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    // Outside Valgrind these are behavioral no-ops; the tests pin that the
122    // helpers never alter values or crash, with and without `__ctgrind`.
123    #[test]
124    fn helpers_preserve_values() {
125        let data = [1u8, 2, 3];
126        mark_secret(&data);
127        declassify(&data);
128        assert_eq!(data, [1, 2, 3]);
129
130        assert!(declassify_choice(subtle::Choice::from(1)));
131        assert!(!declassify_choice(subtle::Choice::from(0)));
132    }
133
134    #[test]
135    fn helpers_accept_empty_slices() {
136        mark_secret(&[]);
137        declassify(&[]);
138    }
139}