Skip to main content

kipuka/config/
otp.rs

1//! One-Time Password (OTP) configuration for EST enrollment.
2//!
3//! OTP authentication provides an alternative to mTLS for initial device
4//! enrollment (RHELBU-3536 R7).  An administrator generates an OTP via
5//! the admin API, which the client presents in an HTTP Basic Authorization
6//! header alongside its CSR.
7//!
8//! OTP storage backends:
9//!
10//! - **db** — OTPs are stored in the Kipuka database.
11//! - **ldap** — OTPs are stored as attributes on LDAP entries, enabling
12//!   integration with FreeIPA or Active Directory enrollment workflows.
13
14use serde::Deserialize;
15
16/// OTP storage backend.
17#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "lowercase")]
19#[derive(Default)]
20pub enum OtpStorageBackend {
21    /// Store OTPs in the Kipuka database.
22    #[default]
23    Db,
24    /// Store OTPs in an LDAP directory.
25    Ldap,
26}
27
28/// `[otp]` section — OTP enrollment authentication configuration.
29///
30/// ```toml
31/// [otp]
32/// enabled = true
33/// entropy_bits = 128
34/// ttl_seconds = 3600
35/// max_usage = 1
36/// storage_backend = "db"
37/// ```
38#[derive(Debug, Clone, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct OtpConfig {
41    /// Enable OTP-based enrollment authentication.
42    #[serde(default)]
43    pub enabled: bool,
44
45    /// Minimum entropy bits for generated OTPs.
46    ///
47    /// NIST SP 800-63B requires at least 112 bits for authenticator
48    /// secrets; the Kipuka default is 128 bits for a comfortable margin.
49    /// Values below 128 are rejected during validation.
50    #[serde(default = "default_entropy_bits")]
51    pub entropy_bits: u32,
52
53    /// Time-to-live for OTPs in seconds.
54    ///
55    /// After this duration, unused OTPs are automatically invalidated.
56    /// Default: 3600 (1 hour).
57    #[serde(default = "default_ttl_seconds")]
58    pub ttl_seconds: u64,
59
60    /// Maximum number of times an OTP can be used before it is consumed.
61    ///
62    /// `1` (the default) enforces single-use semantics.  Values greater
63    /// than 1 allow re-enrollment within the TTL window (e.g., for
64    /// retry after transient failure).
65    #[serde(default = "default_max_usage")]
66    pub max_usage: u32,
67
68    /// Storage backend for OTP records.
69    #[serde(default)]
70    pub storage_backend: OtpStorageBackend,
71
72    /// LDAP connection configuration (required when `storage_backend = "ldap"`).
73    #[serde(default)]
74    pub ldap: Option<OtpLdapConfig>,
75}
76
77/// LDAP backend configuration for OTP storage (RHELBU-3536 R7).
78///
79/// ```toml
80/// [otp.ldap]
81/// url = "ldaps://ipa.example.com"
82/// bind_dn = "uid=kipuka,cn=sysaccounts,cn=etc,dc=example,dc=com"
83/// bind_password = "env:KIPUKA_LDAP_BIND_PW"
84/// base_dn = "cn=otp,cn=kipuka,dc=example,dc=com"
85/// ```
86#[derive(Debug, Clone, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct OtpLdapConfig {
89    /// LDAP server URL (`ldap://` or `ldaps://`).
90    pub url: String,
91
92    /// Bind DN for LDAP authentication.
93    pub bind_dn: String,
94
95    /// Bind password.  Supports `"env:VAR_NAME"` for env-var expansion.
96    #[serde(default)]
97    pub bind_password: String,
98
99    /// Base DN under which OTP entries are stored.
100    pub base_dn: String,
101
102    /// LDAP attribute name for the OTP value.
103    /// Default: `"kipukaOtp"`.
104    #[serde(default = "default_otp_attribute")]
105    pub otp_attribute: String,
106
107    /// Connection timeout in seconds.
108    #[serde(default = "default_ldap_timeout_secs")]
109    pub timeout_secs: u64,
110
111    /// Use STARTTLS over a plain LDAP connection.
112    #[serde(default)]
113    pub starttls: bool,
114}
115
116fn default_entropy_bits() -> u32 {
117    128
118}
119
120fn default_ttl_seconds() -> u64 {
121    3600
122}
123
124fn default_max_usage() -> u32 {
125    1
126}
127
128fn default_otp_attribute() -> String {
129    "kipukaOtp".to_string()
130}
131
132fn default_ldap_timeout_secs() -> u64 {
133    10
134}
135
136impl Default for OtpConfig {
137    fn default() -> Self {
138        Self {
139            enabled: false,
140            entropy_bits: default_entropy_bits(),
141            ttl_seconds: default_ttl_seconds(),
142            max_usage: default_max_usage(),
143            storage_backend: OtpStorageBackend::default(),
144            ldap: None,
145        }
146    }
147}
148
149impl OtpConfig {
150    /// Validate OTP configuration constraints.
151    pub fn validate(&self) -> std::result::Result<(), String> {
152        if !self.enabled {
153            return Ok(());
154        }
155
156        if self.entropy_bits < 128 {
157            return Err(format!(
158                "[otp].entropy_bits must be at least 128, got {}",
159                self.entropy_bits
160            ));
161        }
162
163        if self.ttl_seconds == 0 {
164            return Err("[otp].ttl_seconds must be at least 1".into());
165        }
166
167        if self.max_usage == 0 {
168            return Err("[otp].max_usage must be at least 1".into());
169        }
170
171        if self.storage_backend == OtpStorageBackend::Ldap && self.ldap.is_none() {
172            return Err("[otp].ldap section is required when storage_backend = \"ldap\"".into());
173        }
174
175        if let Some(ref ldap) = self.ldap {
176            if ldap.url.is_empty() {
177                return Err("[otp.ldap].url must not be empty".into());
178            }
179            if ldap.bind_dn.is_empty() {
180                return Err("[otp.ldap].bind_dn must not be empty".into());
181            }
182            if ldap.base_dn.is_empty() {
183                return Err("[otp.ldap].base_dn must not be empty".into());
184            }
185        }
186
187        Ok(())
188    }
189}