noxtls_core/config.rs
1// Copyright (c) 2019-2026, Argenox Technologies LLC
2// All rights reserved.
3//
4// SPDX-License-Identifier: GPL-2.0-only OR LicenseRef-Argenox-Commercial-License
5//
6// This file is part of the NoxTLS Library.
7//
8// This program is free software: you can redistribute it and/or modify
9// it under the terms of the GNU General Public License as published by the
10// Free Software Foundation; version 2 of the License.
11//
12// Alternatively, this file may be used under the terms of a commercial
13// license from Argenox Technologies LLC.
14//
15// See `noxtls/LICENSE` and `noxtls/LICENSE.md` in this repository for full details.
16// CONTACT: info@argenox.com
17
18//! Compile-time and parsed library security configuration: profiles, policy flags, and mbedTLS-style
19//! `#define` inputs. Used by higher-level crates to align runtime behavior with Cargo feature sets.
20
21#[cfg(feature = "std")]
22use std::path::Path;
23
24use crate::{Error, Profile, Result};
25
26/// Selects how aggressively cryptographic code paths avoid data-dependent timing.
27#[derive(Debug, Copy, Clone, Eq, PartialEq)]
28pub enum ConstantTimePolicy {
29 /// Prefer constant-time implementations where available without failing unsupported operations.
30 BestEffort,
31 /// Require strict constant-time behavior where the build policy enables it.
32 Strict,
33}
34
35/// User-tunable security policy switches paired with a [`Profile`].
36#[derive(Debug, Copy, Clone, Eq, PartialEq)]
37pub struct SecurityPolicy {
38 /// Timing-hardening mode derived from Cargo features or parsed configuration.
39 pub constant_time: ConstantTimePolicy,
40 /// Whether legacy algorithms may be used when allowed by build policy.
41 pub allow_legacy_algorithms: bool,
42 /// Whether SHA-1 signatures may be accepted when allowed by build policy.
43 pub allow_sha1_signatures: bool,
44}
45
46/// Top-level NoxTLS library configuration: active profile and effective security policy.
47#[derive(Debug, Copy, Clone, Eq, PartialEq)]
48pub struct LibraryConfig {
49 /// Selected feature profile for TLS/DTLS and crypto surface area.
50 pub profile: Profile,
51 /// Security policy flags validated together with `profile`.
52 pub policy: SecurityPolicy,
53}
54
55/// Returns whether the `policy-strict-constant-time` Cargo feature was enabled at compile time.
56///
57/// # Arguments
58///
59/// This function takes no parameters.
60///
61/// # Returns
62///
63/// `true` when strict constant-time policy is compiled in; `false` otherwise.
64///
65/// # Panics
66///
67/// This function does not panic.
68#[must_use]
69pub fn compiled_strict_constant_time() -> bool {
70 cfg!(feature = "policy-strict-constant-time")
71}
72
73/// Returns whether the `policy-allow-legacy-algorithms` Cargo feature was enabled at compile time.
74///
75/// # Arguments
76///
77/// This function takes no parameters.
78///
79/// # Returns
80///
81/// `true` when legacy algorithms are allowed by the build; `false` otherwise.
82///
83/// # Panics
84///
85/// This function does not panic.
86#[must_use]
87pub fn compiled_allow_legacy_algorithms() -> bool {
88 cfg!(feature = "policy-allow-legacy-algorithms")
89}
90
91/// Returns whether the `policy-allow-sha1-signatures` Cargo feature was enabled at compile time.
92///
93/// # Arguments
94///
95/// This function takes no parameters.
96///
97/// # Returns
98///
99/// `true` when SHA-1 signature compatibility is allowed by the build; `false` otherwise.
100///
101/// # Panics
102///
103/// This function does not panic.
104#[must_use]
105pub fn compiled_allow_sha1_signatures() -> bool {
106 cfg!(feature = "policy-allow-sha1-signatures")
107}
108
109impl SecurityPolicy {
110 /// Builds a [`SecurityPolicy`] from active Cargo feature flags at compile time.
111 ///
112 /// # Arguments
113 ///
114 /// This function takes no parameters.
115 ///
116 /// # Returns
117 ///
118 /// A policy struct whose fields reflect `cfg!(feature = ...)` for constant-time, legacy, and SHA-1 modes.
119 ///
120 /// # Panics
121 ///
122 /// This function does not panic.
123 #[must_use]
124 pub fn compiled() -> Self {
125 let constant_time = if compiled_strict_constant_time() {
126 ConstantTimePolicy::Strict
127 } else {
128 ConstantTimePolicy::BestEffort
129 };
130 Self {
131 constant_time,
132 allow_legacy_algorithms: compiled_allow_legacy_algorithms(),
133 allow_sha1_signatures: compiled_allow_sha1_signatures(),
134 }
135 }
136
137 /// Ensures policy flags are internally consistent (for example, strict constant-time vs legacy modes).
138 ///
139 /// # Arguments
140 ///
141 /// * `self` — Policy snapshot to validate.
142 ///
143 /// # Returns
144 ///
145 /// `Ok(())` when all invariants hold.
146 ///
147 /// # Errors
148 ///
149 /// Returns [`Error::UnsupportedFeature`] when strict constant-time is combined with disallowed legacy or SHA-1 modes.
150 ///
151 /// # Panics
152 ///
153 /// This function does not panic.
154 pub fn validate(self) -> Result<()> {
155 if self.constant_time == ConstantTimePolicy::Strict && self.allow_legacy_algorithms {
156 return Err(Error::UnsupportedFeature(
157 "strict constant-time policy is incompatible with legacy algorithms",
158 ));
159 }
160 if self.constant_time == ConstantTimePolicy::Strict && self.allow_sha1_signatures {
161 return Err(Error::UnsupportedFeature(
162 "strict constant-time policy is incompatible with sha1 signature mode",
163 ));
164 }
165 Ok(())
166 }
167}
168
169impl LibraryConfig {
170 /// Builds the default [`LibraryConfig`] using compile-time policy flags and validates it.
171 ///
172 /// # Arguments
173 ///
174 /// This function takes no parameters.
175 ///
176 /// # Returns
177 ///
178 /// On success, a configuration with [`Profile::Default`] and [`SecurityPolicy::compiled`].
179 ///
180 /// # Errors
181 ///
182 /// Propagates [`Error::UnsupportedFeature`] from [`SecurityPolicy::validate`] when the compiled policy is invalid.
183 ///
184 /// # Panics
185 ///
186 /// This function does not panic.
187 pub fn compiled() -> Result<Self> {
188 let config = Self {
189 profile: Profile::Default,
190 policy: SecurityPolicy::compiled(),
191 };
192 config.validate()?;
193 Ok(config)
194 }
195
196 /// Validates the profile and nested security policy together.
197 ///
198 /// # Arguments
199 ///
200 /// * `self` — Library configuration to check.
201 ///
202 /// # Returns
203 ///
204 /// `Ok(())` when the configuration is consistent.
205 ///
206 /// # Errors
207 ///
208 /// Returns the same errors as [`SecurityPolicy::validate`] when policy invariants fail.
209 ///
210 /// # Panics
211 ///
212 /// This function does not panic.
213 pub fn validate(self) -> Result<()> {
214 self.policy.validate()?;
215 Ok(())
216 }
217
218 /// Parses mbedTLS-style `#define` configuration text into a [`LibraryConfig`].
219 ///
220 /// Recognized profile symbols (at most one may appear): `NOXTLS_PROFILE_DEFAULT`,
221 /// `NOXTLS_PROFILE_MINIMAL_TLS_CLIENT`, `NOXTLS_PROFILE_TLS_SERVER_PKI`, `NOXTLS_PROFILE_CRYPTO_ONLY`,
222 /// `NOXTLS_PROFILE_FIPS_LIKE`, `NOXTLS_PROFILE_UT_ALL_FEATURES`. Policy symbols: `NOXTLS_STRICT_CONSTANT_TIME`,
223 /// `NOXTLS_ALLOW_LEGACY_ALGORITHMS`, `NOXTLS_ALLOW_SHA1_SIGNATURES`. Lines may include `//` or `/*` inline comments.
224 ///
225 /// # Arguments
226 ///
227 /// * `input` — Full configuration text scanned line-by-line for supported `#define` directives.
228 ///
229 /// # Returns
230 ///
231 /// On success, a validated configuration; if no profile symbol is present, [`Profile::Default`] is used.
232 ///
233 /// # Errors
234 ///
235 /// Returns [`Error::ParseFailure`] for duplicate profiles, unknown symbols, or malformed `#define` lines.
236 ///
237 /// Returns [`Error::UnsupportedFeature`] when parsed policy violates the same rules as [`SecurityPolicy::validate`].
238 ///
239 /// # Panics
240 ///
241 /// This function does not panic.
242 pub fn from_mbedtls_style_str(input: &str) -> Result<Self> {
243 let mut profile: Option<Profile> = None;
244 let mut policy = SecurityPolicy {
245 constant_time: ConstantTimePolicy::BestEffort,
246 allow_legacy_algorithms: false,
247 allow_sha1_signatures: false,
248 };
249
250 for (line_idx, raw_line) in input.lines().enumerate() {
251 let line = strip_inline_comment(raw_line).trim();
252 if line.is_empty() {
253 continue;
254 }
255 let symbol = match parse_define_symbol(line) {
256 Some(value) => value,
257 None => continue,
258 };
259 match symbol {
260 "NOXTLS_PROFILE_DEFAULT" => {
261 set_profile_once(&mut profile, Profile::Default, line_idx + 1)?
262 }
263 "NOXTLS_PROFILE_MINIMAL_TLS_CLIENT" => {
264 set_profile_once(&mut profile, Profile::MinimalTlsClient, line_idx + 1)?
265 }
266 "NOXTLS_PROFILE_TLS_SERVER_PKI" => {
267 set_profile_once(&mut profile, Profile::TlsServerPki, line_idx + 1)?
268 }
269 "NOXTLS_PROFILE_CRYPTO_ONLY" => {
270 set_profile_once(&mut profile, Profile::CryptoOnly, line_idx + 1)?
271 }
272 "NOXTLS_PROFILE_FIPS_LIKE" => {
273 set_profile_once(&mut profile, Profile::FipsLike, line_idx + 1)?
274 }
275 "NOXTLS_PROFILE_UT_ALL_FEATURES" => {
276 set_profile_once(&mut profile, Profile::UtAllFeatures, line_idx + 1)?
277 }
278 "NOXTLS_STRICT_CONSTANT_TIME" => {
279 policy.constant_time = ConstantTimePolicy::Strict;
280 }
281 "NOXTLS_ALLOW_LEGACY_ALGORITHMS" => {
282 policy.allow_legacy_algorithms = true;
283 }
284 "NOXTLS_ALLOW_SHA1_SIGNATURES" => {
285 policy.allow_sha1_signatures = true;
286 }
287 _ => {
288 return Err(Error::ParseFailure(
289 "unsupported noxtls configuration symbol",
290 ));
291 }
292 }
293 }
294
295 let config = Self {
296 profile: profile.unwrap_or(Profile::Default),
297 policy,
298 };
299 config.validate()?;
300 Ok(config)
301 }
302
303 /// Reads a file from disk and parses it with [`LibraryConfig::from_mbedtls_style_str`].
304 ///
305 /// # Arguments
306 ///
307 /// * `path` — Filesystem path to a UTF-8 text file containing mbedTLS-style `#define` lines.
308 ///
309 /// # Returns
310 ///
311 /// On success, the parsed and validated [`LibraryConfig`].
312 ///
313 /// # Errors
314 ///
315 /// Returns [`Error::ParseFailure`] when the file cannot be read as UTF-8 or when text parsing fails.
316 ///
317 /// Returns [`Error::UnsupportedFeature`] when parsed policy fails validation.
318 ///
319 /// # Panics
320 ///
321 /// This function does not panic.
322 #[cfg(feature = "std")]
323 pub fn from_mbedtls_style_file(path: &Path) -> Result<Self> {
324 let content = std::fs::read_to_string(path)
325 .map_err(|_| Error::ParseFailure("failed to read noxtls configuration file"))?;
326 Self::from_mbedtls_style_str(&content)
327 }
328}
329
330/// Removes trailing C/C++ style inline comments from one configuration source line.
331///
332/// # Arguments
333///
334/// * `line` — Raw line possibly containing `//` or `/*` comment starters.
335///
336/// # Returns
337///
338/// The substring before the first comment introducer, or `line` unchanged when none appear.
339///
340/// # Panics
341///
342/// This function does not panic.
343fn strip_inline_comment(line: &str) -> &str {
344 if let Some((content, _)) = line.split_once("//") {
345 return content;
346 }
347 if let Some((content, _)) = line.split_once("/*") {
348 return content;
349 }
350 line
351}
352
353/// Parses a whitespace-split line for `#define NOXTLS_...` and returns the symbol name.
354///
355/// # Arguments
356///
357/// * `line` — Trimmed or partially trimmed configuration line (without guaranteed leading `#` spacing normalized).
358///
359/// # Returns
360///
361/// `Some(symbol)` when the line is a `#define` whose symbol starts with `NOXTLS_`; `None` for other shapes.
362///
363/// # Panics
364///
365/// This function does not panic.
366fn parse_define_symbol(line: &str) -> Option<&str> {
367 let mut parts = line.split_whitespace();
368 if parts.next()? != "#define" {
369 return None;
370 }
371 let symbol = parts.next()?;
372 if symbol.starts_with("NOXTLS_") {
373 Some(symbol)
374 } else {
375 None
376 }
377}
378
379/// Assigns `value` into `slot` when empty, or returns an error if a profile was already chosen.
380///
381/// # Arguments
382///
383/// * `slot` — Optional profile storage updated on first successful call.
384/// * `value` — Profile variant derived from the current `#define` line.
385/// * `_line_number` — Reserved for future diagnostics (1-based source line index).
386///
387/// # Returns
388///
389/// `Ok(())` after storing `value`, or `Err` when `slot` already holds a profile.
390///
391/// # Errors
392///
393/// Returns [`Error::ParseFailure`] when more than one profile symbol is defined in one file.
394///
395/// # Panics
396///
397/// This function does not panic.
398fn set_profile_once(slot: &mut Option<Profile>, value: Profile, _line_number: usize) -> Result<()> {
399 if slot.is_some() {
400 return Err(Error::ParseFailure(
401 "multiple noxtls profile defines found in configuration",
402 ));
403 }
404 *slot = Some(value);
405 Ok(())
406}