Skip to main content

kipuka_dogtag/
profiles.rs

1//! Enrollment profile operations via Dogtag CA REST API.
2//!
3//! Provides profile enumeration and constraint extraction from
4//! `/ca/rest/profiles`. Profile constraints are used by kipuka's
5//! `/csrattrs` endpoint to derive CSR attribute hints per RFC 7030 S4.5.
6
7use serde::Deserialize;
8use tracing::debug;
9
10use crate::DogtagResult;
11use crate::client::DogtagClient;
12
13/// Summary information about an enrollment profile.
14#[derive(Debug, Clone, Deserialize)]
15#[serde(rename_all = "PascalCase")]
16pub struct ProfileInfo {
17    /// Profile identifier (e.g., "caServerCert").
18    pub profile_id: String,
19    /// Human-readable profile name.
20    #[serde(default)]
21    pub name: Option<String>,
22    /// Profile description.
23    #[serde(default)]
24    pub description: Option<String>,
25    /// Whether the profile is enabled.
26    #[serde(default)]
27    pub enabled: Option<bool>,
28    /// Whether the profile is visible to end entities.
29    #[serde(default)]
30    pub visible: Option<bool>,
31}
32
33/// Detailed profile definition including inputs, outputs, and policy sets.
34#[derive(Debug, Clone, Deserialize)]
35#[serde(rename_all = "PascalCase")]
36pub struct ProfileDetail {
37    /// Profile identifier.
38    pub profile_id: String,
39    /// Human-readable name.
40    #[serde(default)]
41    pub name: Option<String>,
42    /// Profile description.
43    #[serde(default)]
44    pub description: Option<String>,
45    /// Whether the profile is enabled.
46    #[serde(default)]
47    pub enabled: Option<bool>,
48    /// Policy set constraints defining the certificate structure.
49    #[serde(default)]
50    pub policy_sets: Vec<PolicySet>,
51}
52
53/// A named group of policies within a profile.
54#[derive(Debug, Clone, Deserialize)]
55#[serde(rename_all = "PascalCase")]
56pub struct PolicySet {
57    /// Policy set identifier.
58    #[serde(default)]
59    pub id: Option<String>,
60    /// Individual policies in this set.
61    #[serde(default)]
62    pub policies: Vec<Policy>,
63}
64
65/// A single profile policy (constraint or default).
66#[derive(Debug, Clone, Deserialize)]
67#[serde(rename_all = "PascalCase")]
68pub struct Policy {
69    /// Policy identifier.
70    #[serde(default)]
71    pub id: Option<String>,
72    /// Constraint definition.
73    #[serde(default)]
74    pub constraint: Option<Constraint>,
75    /// Default values.
76    #[serde(default)]
77    pub defaults: Vec<PolicyDefault>,
78}
79
80/// A constraint within a profile policy.
81#[derive(Debug, Clone, Deserialize)]
82#[serde(rename_all = "PascalCase")]
83pub struct Constraint {
84    /// Constraint class (e.g., "keyConstraintImpl").
85    #[serde(default)]
86    pub class_id: Option<String>,
87    /// Constraint parameters.
88    #[serde(default)]
89    pub params: Vec<ConstraintParam>,
90}
91
92/// A single constraint parameter (name-value pair).
93#[derive(Debug, Clone, Deserialize)]
94#[serde(rename_all = "PascalCase")]
95pub struct ConstraintParam {
96    /// Parameter name (e.g., "keyType", "keyParameters").
97    pub name: String,
98    /// Parameter value.
99    #[serde(default)]
100    pub value: Option<String>,
101}
102
103/// Default value within a policy.
104#[derive(Debug, Clone, Deserialize)]
105#[serde(rename_all = "PascalCase")]
106pub struct PolicyDefault {
107    /// Default class (e.g., "keyUsageExtDefaultImpl").
108    #[serde(default)]
109    pub class_id: Option<String>,
110    /// Default parameters.
111    #[serde(default)]
112    pub params: Vec<ConstraintParam>,
113}
114
115/// Extracted constraints from a profile, suitable for deriving CSR attributes.
116///
117/// Used by kipuka's `/csrattrs` endpoint to tell EST clients what to
118/// include in their certificate signing requests.
119#[derive(Debug, Clone, Default)]
120pub struct ProfileConstraints {
121    /// Allowed key types (e.g., "RSA", "EC", "ML-DSA").
122    pub key_types: Vec<String>,
123    /// Allowed key sizes or named curves (e.g., "2048", "P-256", "ML-DSA-65").
124    pub key_parameters: Vec<String>,
125    /// Key usage flags (e.g., "digitalSignature", "keyEncipherment").
126    pub key_usage: Vec<String>,
127    /// Extended key usage OIDs (e.g., "1.3.6.1.5.5.7.3.1" for TLS server).
128    pub extended_key_usage: Vec<String>,
129    /// Required subject DN components (e.g., "CN", "O", "OU").
130    pub subject_dn_components: Vec<String>,
131}
132
133/// Response from profile listing.
134#[derive(Deserialize)]
135#[serde(rename_all = "PascalCase")]
136struct ProfileListResponse {
137    #[serde(default)]
138    entries: Vec<ProfileInfo>,
139}
140
141impl DogtagClient {
142    /// List all enrollment profiles.
143    ///
144    /// Sends `GET /ca/rest/profiles` and returns summary information
145    /// for each profile. Only enabled and visible profiles are typically
146    /// relevant for EST enrollment.
147    pub async fn list_profiles(&self) -> DogtagResult<Vec<ProfileInfo>> {
148        debug!("Listing enrollment profiles");
149        let resp = self.get("/ca/rest/profiles").await?;
150        let list: ProfileListResponse = Self::json_response(resp).await?;
151        Ok(list.entries)
152    }
153
154    /// Get detailed profile definition by ID.
155    ///
156    /// Sends `GET /ca/rest/profiles/{id}` and returns the full profile
157    /// definition including policy sets, constraints, and defaults.
158    pub async fn get_profile(&self, id: &str) -> DogtagResult<ProfileDetail> {
159        debug!(profile = id, "Fetching profile detail");
160        let resp = self.get(&format!("/ca/rest/profiles/{id}")).await?;
161        Self::json_response(resp).await
162    }
163
164    /// Extract CSR-relevant constraints from a profile.
165    ///
166    /// Parses the profile's policy sets to extract key type constraints,
167    /// key usage extensions, and subject DN requirements. The returned
168    /// [`ProfileConstraints`] can be translated into EST CSR attributes
169    /// for the `/csrattrs` endpoint.
170    pub async fn get_profile_constraints(&self, id: &str) -> DogtagResult<ProfileConstraints> {
171        let detail = self.get_profile(id).await?;
172        Ok(extract_constraints(&detail))
173    }
174}
175
176/// Parse profile policy sets to extract enrollment constraints.
177fn extract_constraints(profile: &ProfileDetail) -> ProfileConstraints {
178    let mut constraints = ProfileConstraints::default();
179
180    for policy_set in &profile.policy_sets {
181        for policy in &policy_set.policies {
182            // Extract key constraints.
183            if let Some(ref constraint) = policy.constraint {
184                if constraint.class_id.as_deref() == Some("keyConstraintImpl") {
185                    for param in &constraint.params {
186                        match param.name.as_str() {
187                            "keyType" => {
188                                if let Some(ref v) = param.value {
189                                    constraints.key_types.push(v.clone());
190                                }
191                            }
192                            "keyParameters" => {
193                                if let Some(ref v) = param.value {
194                                    for p in v.split(',') {
195                                        constraints.key_parameters.push(p.trim().to_owned());
196                                    }
197                                }
198                            }
199                            _ => {}
200                        }
201                    }
202                }
203            }
204
205            // Extract defaults (key usage, EKU, subject DN).
206            for default in &policy.defaults {
207                match default.class_id.as_deref() {
208                    Some("keyUsageExtDefaultImpl") => {
209                        for param in &default.params {
210                            if param.value.as_deref() == Some("true") {
211                                constraints.key_usage.push(param.name.clone());
212                            }
213                        }
214                    }
215                    Some("extendedKeyUsageExtDefaultImpl") => {
216                        for param in &default.params {
217                            if let Some(ref v) = param.value {
218                                constraints.extended_key_usage.push(v.clone());
219                            }
220                        }
221                    }
222                    Some("subjectNameDefaultImpl") | Some("nsSubjectNameDefaultImpl") => {
223                        for param in &default.params {
224                            if param.name == "name" {
225                                if let Some(ref v) = param.value {
226                                    // Extract DN component names (CN, O, OU, etc.).
227                                    for component in v.split(',') {
228                                        if let Some(name) = component.split('=').next() {
229                                            let name = name.trim();
230                                            if !name.is_empty()
231                                                && !constraints
232                                                    .subject_dn_components
233                                                    .contains(&name.to_owned())
234                                            {
235                                                constraints
236                                                    .subject_dn_components
237                                                    .push(name.to_owned());
238                                            }
239                                        }
240                                    }
241                                }
242                            }
243                        }
244                    }
245                    _ => {}
246                }
247            }
248        }
249    }
250
251    constraints
252}