1use serde::{Deserialize, Serialize};
9use tracing::debug;
10
11use crate::client::DogtagClient;
12use crate::{DogtagError, DogtagResult};
13
14#[derive(Debug, Clone)]
16pub struct EnrollResult {
17 pub request_id: String,
19 pub status: EnrollStatus,
21 pub certificate_der: Option<Vec<u8>>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum EnrollStatus {
29 Complete,
31 Pending,
33 Rejected,
35 Canceled,
37}
38
39#[derive(Serialize)]
43#[serde(rename_all = "PascalCase")]
44struct EnrollmentRequest {
45 profile_id: String,
47 renewal: bool,
49 input: Vec<ProfileInput>,
51}
52
53#[derive(Serialize)]
54#[serde(rename_all = "PascalCase")]
55struct ProfileInput {
56 class_id: String,
58 attributes: Vec<ProfileAttribute>,
60}
61
62#[derive(Serialize)]
63#[serde(rename_all = "PascalCase")]
64struct ProfileAttribute {
65 name: String,
67 value: String,
69}
70
71#[derive(Deserialize)]
73#[serde(rename_all = "PascalCase")]
74struct EnrollmentResponse {
75 #[serde(default)]
77 entries: Vec<EnrollmentEntry>,
78}
79
80#[derive(Deserialize)]
81#[serde(rename_all = "PascalCase")]
82struct EnrollmentEntry {
83 request_id: Option<String>,
84 request_status: Option<String>,
85 #[serde(default)]
86 cert_id: Option<String>,
87}
88
89#[derive(Deserialize)]
91#[serde(rename_all = "PascalCase")]
92struct CertDataResponse {
93 #[serde(default)]
95 encoded: Option<String>,
96}
97
98impl DogtagClient {
99 pub async fn enroll_certificate(
117 &self,
118 csr_pem: &str,
119 profile_id: &str,
120 ) -> DogtagResult<EnrollResult> {
121 debug!(profile = profile_id, "Submitting enrollment request");
122
123 let request = EnrollmentRequest {
124 profile_id: profile_id.to_owned(),
125 renewal: false,
126 input: vec![ProfileInput {
127 class_id: "certReqInputImpl".to_owned(),
128 attributes: vec![ProfileAttribute {
129 name: "cert_request".to_owned(),
130 value: csr_pem.to_owned(),
131 }],
132 }],
133 };
134
135 let resp = self.post_json("/ca/rest/certrequests", &request).await?;
136 let enrollment: EnrollmentResponse = Self::json_response(resp).await?;
137
138 let entry = enrollment
139 .entries
140 .into_iter()
141 .next()
142 .ok_or_else(|| DogtagError::ParseError("Empty enrollment response".into()))?;
143
144 let request_id = entry
145 .request_id
146 .ok_or_else(|| DogtagError::ParseError("Missing request_id in response".into()))?;
147
148 let status = match entry.request_status.as_deref() {
149 Some("complete") => EnrollStatus::Complete,
150 Some("pending") => EnrollStatus::Pending,
151 Some("rejected") => EnrollStatus::Rejected,
152 Some("canceled") => EnrollStatus::Canceled,
153 Some(other) => {
154 return Err(DogtagError::ParseError(format!(
155 "Unknown request status: {other}"
156 )));
157 }
158 None => {
159 return Err(DogtagError::ParseError(
160 "Missing request_status in response".into(),
161 ));
162 }
163 };
164
165 let certificate_der = if status == EnrollStatus::Complete {
167 if let Some(cert_id) = &entry.cert_id {
168 Some(self.fetch_cert_der(cert_id).await?)
169 } else {
170 None
171 }
172 } else {
173 None
174 };
175
176 Ok(EnrollResult {
177 request_id,
178 status,
179 certificate_der,
180 })
181 }
182
183 pub async fn get_enrollment_status(&self, request_id: &str) -> DogtagResult<EnrollResult> {
190 debug!(request_id, "Checking enrollment status");
191
192 let resp = self
193 .get(&format!("/ca/rest/certrequests/{request_id}"))
194 .await?;
195 let entry: EnrollmentEntry = Self::json_response(resp).await?;
196
197 let status = match entry.request_status.as_deref() {
198 Some("complete") => EnrollStatus::Complete,
199 Some("pending") => EnrollStatus::Pending,
200 Some("rejected") => EnrollStatus::Rejected,
201 Some("canceled") => EnrollStatus::Canceled,
202 other => {
203 return Err(DogtagError::ParseError(format!(
204 "Unknown request status: {other:?}"
205 )));
206 }
207 };
208
209 let certificate_der = if status == EnrollStatus::Complete {
210 if let Some(cert_id) = &entry.cert_id {
211 Some(self.fetch_cert_der(cert_id).await?)
212 } else {
213 None
214 }
215 } else {
216 None
217 };
218
219 Ok(EnrollResult {
220 request_id: request_id.to_owned(),
221 status,
222 certificate_der,
223 })
224 }
225
226 async fn fetch_cert_der(&self, cert_id: &str) -> DogtagResult<Vec<u8>> {
228 let resp = self.get(&format!("/ca/rest/certs/{cert_id}")).await?;
229 let cert_data: CertDataResponse = Self::json_response(resp).await?;
230
231 let encoded = cert_data
232 .encoded
233 .ok_or_else(|| DogtagError::ParseError("Missing certificate data".into()))?;
234
235 let b64: String = encoded
237 .lines()
238 .filter(|l| !l.starts_with("-----"))
239 .collect();
240
241 use base64::Engine;
242 base64::engine::general_purpose::STANDARD
243 .decode(&b64)
244 .map_err(|e| DogtagError::ParseError(format!("Invalid base64 in certificate: {e}")))
245 }
246}