kipuka/config/est.rs
1//! EST protocol configuration.
2//!
3//! The `[est]` section controls which EST operations are enabled globally,
4//! and `[[est.label]]` entries define per-label enrollment profiles with
5//! CA routing and authentication policies.
6//!
7//! # EST labels (RFC 7030 §3.2.2)
8//!
9//! EST labels provide a namespace mechanism for multiple enrollment profiles
10//! under the same server. Each label maps to a URL path segment:
11//!
12//! ```text
13//! https://est.example.com/.well-known/est/{label}/simpleenroll
14//! ```
15//!
16//! When no label is specified in the URL, the default label configuration
17//! applies.
18
19use serde::Deserialize;
20
21/// CSR template mode configuration (draft-ietf-lamps-rfc7030-csrattrs / RFC 9908).
22///
23/// When configured, the `/csrattrs` response includes a
24/// `CertificationRequestInfoTemplate` attribute alongside the OID list,
25/// guiding clients on expected subject DN, key algorithm, and extensions.
26///
27/// The template is backward-compatible: clients that do not understand
28/// the template attribute will ignore it and process only the OID list.
29///
30/// ```toml
31/// [est.csr_template]
32/// key_algorithm = "ec:P-256"
33/// required_extensions = ["2.5.29.17"]
34///
35/// [[est.csr_template.subject]]
36/// oid = "2.5.4.10"
37/// value = "Example Corp"
38///
39/// [[est.csr_template.subject]]
40/// oid = "2.5.4.3"
41/// ```
42#[derive(Debug, Clone, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct CsrTemplate {
45 /// Required subject DN components.
46 ///
47 /// Each entry specifies an OID and an optional value. When `value` is
48 /// `None`, the client must supply its own value for that RDN component.
49 #[serde(default)]
50 pub subject: Vec<CsrTemplateRdn>,
51
52 /// Required key algorithm constraint.
53 ///
54 /// Format: `"ec:P-256"`, `"ec:P-384"`, `"rsa:2048"`, `"rsa:4096"`.
55 /// When set, encodes a `SubjectPublicKeyInfoTemplate` in the template.
56 #[serde(default)]
57 pub key_algorithm: Option<String>,
58
59 /// Required X.509 extension OIDs (client fills values).
60 ///
61 /// Each entry is a dotted-decimal OID string (e.g., `"2.5.29.17"`
62 /// for subjectAltName).
63 #[serde(default)]
64 pub required_extensions: Vec<String>,
65}
66
67impl CsrTemplate {
68 /// Returns `true` when at least one template field carries content.
69 pub fn has_content(&self) -> bool {
70 !self.subject.is_empty()
71 || self.key_algorithm.is_some()
72 || !self.required_extensions.is_empty()
73 }
74}
75
76/// A single RDN component in the CSR template subject.
77#[derive(Debug, Clone, Deserialize)]
78#[serde(deny_unknown_fields)]
79pub struct CsrTemplateRdn {
80 /// OID of the attribute type (dotted-decimal, e.g., `"2.5.4.3"` for CN).
81 pub oid: String,
82 /// Pre-filled value. When `None`, the client must provide this field.
83 pub value: Option<String>,
84}
85
86/// Authentication method for EST enrollment requests.
87///
88/// RFC 7030 §3.2.3 defines several client authentication mechanisms.
89/// Each EST label can require a specific method or accept multiple.
90#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
91#[serde(rename_all = "lowercase")]
92pub enum EstAuthMethod {
93 /// CMS message authentication for the custom CMS endpoints.
94 Cms,
95 /// GSSAPI over TLS with channel binding.
96 Gssapi,
97 /// mTLS client certificate authentication (RFC 7030 §3.3.2).
98 Mtls,
99 /// HTTP Basic authentication with OTP (RHELBU-3536 R7).
100 Otp,
101 /// HTTP Basic authentication with static credentials.
102 Basic,
103 /// Certificate-based re-enrollment (existing certificate proves identity).
104 Certificate,
105}
106
107/// `[est]` section — global EST protocol settings.
108#[derive(Debug, Clone, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct EstConfig {
111 /// Enable the `/simpleenroll` endpoint (RFC 7030 §4.2).
112 #[serde(default = "bool_true")]
113 pub simpleenroll: bool,
114
115 /// Enable the `/simplereenroll` endpoint (RFC 7030 §4.2.2).
116 #[serde(default = "bool_true")]
117 pub simplereenroll: bool,
118
119 /// Enable the `/fullcmc` endpoint (RFC 7030 §4.3).
120 ///
121 /// Full CMC is rarely needed; disabled by default.
122 #[serde(default)]
123 pub fullcmc: bool,
124
125 /// PEM file containing trust anchors for CMC RA signer verification.
126 ///
127 /// When set, `/fullcmc` verifies the CMS SignedData signature against
128 /// these certificates instead of only the target CA cert. This allows
129 /// RA certificates issued by a different CA or intermediate to be
130 /// accepted. When absent, the target CA certificate is used as the
131 /// sole trust anchor.
132 #[serde(default)]
133 pub cmc_truststore_file: Option<String>,
134
135 /// Enable the `/serverkeygen` endpoint (RFC 7030 §4.4).
136 ///
137 /// Server-side key generation requires HSM integration.
138 /// Disabled by default.
139 #[serde(default)]
140 pub serverkeygen: bool,
141
142 /// Enable the `/csrattrs` endpoint (RFC 7030 §4.5).
143 #[serde(default = "bool_true")]
144 pub csrattrs: bool,
145
146 /// Default enrollment profile applied when no label is specified.
147 ///
148 /// When absent, enrollment requests without a label use the default
149 /// CA and authentication policy.
150 #[serde(default)]
151 pub default_profile: Option<String>,
152
153 /// CSR attribute hints returned by `/csrattrs`.
154 ///
155 /// Each entry is an OID string (e.g., `"1.2.840.113549.1.9.14"` for
156 /// the Certificate Extensions Request attribute).
157 #[serde(default)]
158 pub csr_attributes: Vec<String>,
159
160 /// CSR template mode (draft-ietf-lamps-rfc7030-csrattrs / RFC 9908).
161 ///
162 /// When set, the `/csrattrs` response includes a
163 /// `CertificationRequestInfoTemplate` attribute that tells the client
164 /// which subject DN fields, key algorithm, and extensions are required.
165 /// This coexists with the OID-list mode.
166 #[serde(default)]
167 pub csr_template: Option<CsrTemplate>,
168
169 /// Per-label enrollment configurations.
170 #[serde(default, rename = "label")]
171 pub labels: Vec<EstLabelConfig>,
172
173 /// Disconnected mode: accept enrollment requests without upstream
174 /// CA connectivity (RHELBU-3536 R7-Disconnected).
175 ///
176 /// When `true`, the server queues CSRs for deferred signing and
177 /// returns `202 Accepted` with a `Retry-After` header instead of
178 /// the signed certificate.
179 #[serde(default)]
180 pub disconnected: bool,
181
182 /// Retry-After value (seconds) returned in disconnected mode.
183 /// Default: 300 (5 minutes).
184 #[serde(default = "default_retry_after_secs")]
185 pub disconnected_retry_after_secs: u64,
186
187 /// Number of days before certificate expiry to start the suggested
188 /// renewal window (draft-ietf-lamps-est-renewal-info).
189 ///
190 /// The renewal window starts at `not_after - renewal_window_days` and
191 /// ends one day before `not_after`.
192 #[serde(default = "default_renewal_window_days")]
193 pub renewal_window_days: u32,
194
195 /// `Retry-After` value (seconds) returned in renewal-info responses
196 /// to control client polling frequency.
197 ///
198 /// Default: 86400 (1 day).
199 #[serde(default = "default_renewal_retry_after")]
200 pub renewal_retry_after_secs: u64,
201}
202
203/// `[[est.label]]` — per-label enrollment profile.
204///
205/// Each label provides an independent enrollment namespace with its own
206/// CA routing, authentication requirements, and CSR attribute set.
207///
208/// ```toml
209/// [[est.label]]
210/// name = "devices"
211/// ca_id = "device-ca"
212/// auth_methods = ["mtls", "otp"]
213/// require_cn_match = true
214/// ```
215#[derive(Debug, Clone, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct EstLabelConfig {
218 /// Label name used in the URL path.
219 ///
220 /// Must be a non-empty string matching `^[a-z0-9][a-z0-9_-]*$`.
221 pub name: String,
222
223 /// CA identifier to use for enrollments under this label.
224 ///
225 /// Must reference a `[[ca]]` entry by its `id` field.
226 /// When absent, the default CA is used.
227 pub ca_id: Option<String>,
228
229 /// Explicit authorization for alternate issuers under this label.
230 #[serde(default)]
231 pub ca_pool: Vec<String>,
232
233 /// Allowed authentication methods for this label.
234 ///
235 /// When empty, all globally-enabled auth methods are accepted.
236 #[serde(default)]
237 pub auth_methods: Vec<EstAuthMethod>,
238
239 /// Per-label CSR attribute hints (overrides global `csr_attributes`
240 /// for this label).
241 #[serde(default)]
242 pub csr_attributes: Vec<String>,
243
244 /// Per-label CSR template (overrides global `csr_template` for this label).
245 #[serde(default)]
246 pub csr_template: Option<CsrTemplate>,
247
248 /// Require that the CSR Common Name matches the authenticated identity.
249 ///
250 /// When `true`, the server rejects CSRs where the CN does not match
251 /// the client's authenticated principal name. NIAP CA PP FDP_ACF.1
252 /// identity binding — enforced in `crate::auth::enroll_authz`.
253 #[serde(default)]
254 pub require_cn_match: bool,
255
256 /// Require the authenticated identity to appear as a Subject Alternative
257 /// Name entry (dNSName, rfc822Name, or iPAddress, matched by type).
258 ///
259 /// NIAP CA PP FDP_ACF.1 identity binding. Complements
260 /// `require_cn_match`; both may be set. Default: `false`.
261 #[serde(default)]
262 pub require_san_match: bool,
263
264 /// Name-authorization allowlist of permitted dNSName patterns (RFC 6125,
265 /// wildcards allowed). When non-empty, every dNSName SAN in the CSR must
266 /// match one entry, bounding this label to a fixed namespace.
267 #[serde(default)]
268 pub permitted_dns_names: Vec<String>,
269
270 /// Name-authorization allowlist of permitted iPAddress SANs (textual
271 /// form, e.g. `"10.0.0.1"` or `"2001:db8::1"`). When non-empty, every
272 /// iPAddress SAN in the CSR must equal one entry.
273 #[serde(default)]
274 pub permitted_ip_addresses: Vec<String>,
275
276 /// Name-authorization allowlist of permitted rfc822Name patterns (full
277 /// address or bare domain). When non-empty, every rfc822Name SAN in the
278 /// CSR must match one entry.
279 #[serde(default)]
280 pub permitted_emails: Vec<String>,
281
282 /// Maximum validity period (days) for certificates issued under this label.
283 ///
284 /// Overrides the CA's default `validity_days` for this label.
285 pub max_validity_days: Option<u32>,
286
287 /// Enable disconnected mode for this specific label.
288 /// Overrides the global `[est].disconnected` setting.
289 pub disconnected: Option<bool>,
290}
291
292fn bool_true() -> bool {
293 true
294}
295
296fn default_retry_after_secs() -> u64 {
297 300
298}
299
300fn default_renewal_window_days() -> u32 {
301 30
302}
303
304fn default_renewal_retry_after() -> u64 {
305 86400
306}
307
308impl Default for EstConfig {
309 fn default() -> Self {
310 Self {
311 simpleenroll: true,
312 simplereenroll: true,
313 fullcmc: false,
314 cmc_truststore_file: None,
315 serverkeygen: false,
316 csrattrs: true,
317 default_profile: None,
318 csr_attributes: Vec::new(),
319 csr_template: None,
320 labels: Vec::new(),
321 disconnected: false,
322 disconnected_retry_after_secs: default_retry_after_secs(),
323 renewal_window_days: default_renewal_window_days(),
324 renewal_retry_after_secs: default_renewal_retry_after(),
325 }
326 }
327}