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}