1use serde::{Deserialize, Serialize};
7use tracing::debug;
8
9use crate::client::DogtagClient;
10use crate::{DogtagError, DogtagResult};
11
12#[derive(Debug, Clone, Deserialize)]
14#[serde(rename_all = "PascalCase")]
15pub struct CertInfo {
16 pub id: String,
18 #[serde(default)]
20 pub subject_d_n: Option<String>,
21 #[serde(default)]
23 pub issuer_d_n: Option<String>,
24 #[serde(default)]
26 pub status: Option<String>,
27 #[serde(default)]
29 pub not_valid_before: Option<String>,
30 #[serde(default)]
32 pub not_valid_after: Option<String>,
33 #[serde(default)]
35 pub encoded: Option<String>,
36}
37
38#[derive(Debug, Default, Serialize)]
40pub struct CertFilter {
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub subject: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub status: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub size: Option<u32>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub start: Option<u32>,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
58pub enum RevocationReason {
59 KeyCompromise,
61 CaCompromise,
63 AffiliationChanged,
65 Superseded,
67 CessationOfOperation,
69 CertificateHold,
71 RemoveFromCrl,
73 PrivilegeWithdrawn,
75 AaCompromise,
77 Unspecified,
79}
80
81impl RevocationReason {
82 fn as_code(self) -> u32 {
84 match self {
85 Self::Unspecified => 0,
86 Self::KeyCompromise => 1,
87 Self::CaCompromise => 2,
88 Self::AffiliationChanged => 3,
89 Self::Superseded => 4,
90 Self::CessationOfOperation => 5,
91 Self::CertificateHold => 6,
92 Self::RemoveFromCrl => 8,
93 Self::PrivilegeWithdrawn => 9,
94 Self::AaCompromise => 10,
95 }
96 }
97}
98
99#[derive(Serialize)]
101#[serde(rename_all = "PascalCase")]
102struct RevokeRequest {
103 reason: u32,
104}
105
106#[derive(Deserialize)]
108#[serde(rename_all = "PascalCase")]
109struct CertListResponse {
110 #[serde(default)]
111 entries: Vec<CertInfo>,
112}
113
114impl DogtagClient {
115 pub async fn get_certificate(&self, serial: &str) -> DogtagResult<CertInfo> {
120 debug!(serial, "Fetching certificate");
121 let resp = self.get(&format!("/ca/rest/certs/{serial}")).await?;
122 Self::json_response(resp).await
123 }
124
125 pub async fn revoke_certificate(
133 &self,
134 serial: &str,
135 reason: RevocationReason,
136 ) -> DogtagResult<()> {
137 debug!(serial, reason = ?reason, "Revoking certificate");
138
139 let body = RevokeRequest {
140 reason: reason.as_code(),
141 };
142
143 let resp = self
144 .post_json(&format!("/ca/rest/agent/certs/{serial}/revoke"), &body)
145 .await?;
146
147 if !resp.status().is_success() {
148 let status = resp.status().as_u16();
149 let body = resp.text().await.unwrap_or_default();
150 return Err(DogtagError::ApiError { status, body });
151 }
152
153 Ok(())
154 }
155
156 pub async fn list_certificates(&self, filter: CertFilter) -> DogtagResult<Vec<CertInfo>> {
161 debug!(?filter, "Listing certificates");
162
163 let mut query_parts = Vec::new();
165 if let Some(ref subject) = filter.subject {
166 query_parts.push(format!("subject={subject}"));
167 }
168 if let Some(ref status) = filter.status {
169 query_parts.push(format!("status={status}"));
170 }
171 if let Some(size) = filter.size {
172 query_parts.push(format!("size={size}"));
173 }
174 if let Some(start) = filter.start {
175 query_parts.push(format!("start={start}"));
176 }
177
178 let path = if query_parts.is_empty() {
179 "/ca/rest/certs".to_owned()
180 } else {
181 format!("/ca/rest/certs?{}", query_parts.join("&"))
182 };
183
184 let resp = self.get(&path).await?;
185 let list: CertListResponse = Self::json_response(resp).await?;
186 Ok(list.entries)
187 }
188}