Skip to main content

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}