wire_e2e_identity/acme/
account.rs1use rusty_jwt_tools::prelude::*;
2
3use crate::acme::prelude::*;
4
5impl RustyAcme {
6 pub fn new_account_request(
8 directory: &AcmeDirectory,
9 alg: JwsAlgorithm,
10 kp: &Pem,
11 previous_nonce: String,
12 ) -> RustyAcmeResult<AcmeJws> {
13 const DEFAULT_CONTACT: &str = "anonymous@anonymous.invalid";
14
15 let contact = vec![DEFAULT_CONTACT.to_string()];
18 let payload = AcmeAccountRequest {
19 terms_of_service_agreed: Some(true),
20 contact,
21 only_return_existing: Some(false),
22 };
23 let req = AcmeJws::new(alg, previous_nonce, &directory.new_account, None, Some(payload), kp)?;
24 Ok(req)
25 }
26
27 pub fn new_account_response(response: serde_json::Value) -> RustyAcmeResult<AcmeAccount> {
29 let account = serde_json::from_value::<AcmeAccount>(response)
30 .map_err(|_| RustyAcmeError::SmallstepImplementationError("Invalid account response"))?;
31 account.verify()?;
32 Ok(account)
33 }
34}
35
36#[derive(Debug, thiserror::Error)]
37pub enum AcmeAccountError {
38 #[error("Created account is not valid")]
40 Invalid,
41 #[error("Account was revoked by the server")]
43 Revoked,
44 #[error("A client deactivated this account")]
46 Deactivated,
47}
48
49#[derive(Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
52#[cfg_attr(test, derive(Clone))]
53#[serde(rename_all = "camelCase")]
54struct AcmeAccountRequest {
55 #[serde(skip_serializing_if = "Option::is_none")]
58 pub terms_of_service_agreed: Option<bool>,
59 pub contact: Vec<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
68 pub only_return_existing: Option<bool>,
69}
70
71#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct AcmeAccount {
76 pub status: AcmeAccountStatus,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub orders: Option<url::Url>,
79}
80
81impl AcmeAccount {
82 pub fn acct_url(&self) -> RustyAcmeResult<url::Url> {
85 let orders = self
86 .orders
87 .as_ref()
88 .ok_or(RustyAcmeError::SmallstepImplementationError(
89 "Account should have 'orders' url",
90 ))?;
91 let mut orders = orders.clone();
92 if orders.path_segments().and_then(|mut paths| paths.next_back()) == Some("orders") {
93 orders
94 .path_segments_mut()
95 .map_err(|_| RustyAcmeError::ImplementationError)?
96 .pop();
97 Ok(orders)
98 } else {
99 Err(RustyAcmeError::SmallstepImplementationError(
100 "Invalid 'orders' URL in account",
101 ))
102 }
103 }
104
105 fn verify(&self) -> RustyAcmeResult<()> {
107 self.orders
108 .as_ref()
109 .ok_or(RustyAcmeError::SmallstepImplementationError(
110 "Newly created account should have 'orders' url",
111 ))?;
112 match self.status {
113 AcmeAccountStatus::Valid => Ok(()),
114 AcmeAccountStatus::Deactivated => Err(AcmeAccountError::Deactivated)?,
115 AcmeAccountStatus::Revoked => Err(AcmeAccountError::Revoked)?,
116 }
117 }
118}
119
120#[cfg(test)]
121impl Default for AcmeAccount {
122 fn default() -> Self {
123 Self {
124 status: AcmeAccountStatus::Valid,
125 orders: Some(
126 "https://acme-server/acme/account/muYiJmuJRn9u2L0tdI5bu11T7QqqPR1u/orders"
127 .parse()
128 .unwrap(),
129 ),
130 }
131 }
132}
133
134#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
136#[serde(rename_all = "lowercase")]
137pub enum AcmeAccountStatus {
138 Valid,
139 Deactivated,
140 Revoked,
141}
142
143#[cfg(test)]
144pub mod tests {
145 use serde_json::json;
146 use wasm_bindgen_test::*;
147
148 use super::*;
149
150 wasm_bindgen_test_configure!(run_in_browser);
151
152 mod json {
153 use super::*;
154
155 #[test]
156 #[wasm_bindgen_test]
157 fn can_deserialize_rfc_sample_request() {
158 let rfc_sample = json!({
159 "termsOfServiceAgreed": true,
160 "contact": [
161 "mailto:cert-admin@example.org",
162 "mailto:admin@example.org"
163 ]
164 });
165 assert!(serde_json::from_value::<AcmeAccountRequest>(rfc_sample).is_ok());
166 }
167
168 #[test]
169 #[wasm_bindgen_test]
170 fn can_deserialize_rfc_sample_response() {
171 let rfc_sample = json!({
172 "status": "valid",
173 "contact": [
174 "mailto:cert-admin@example.org",
175 "mailto:admin@example.org"
176 ],
177 "orders": "https://example.com/acme/acct/evOfKhNU60wg/orders"
178 });
179 assert!(serde_json::from_value::<AcmeAccount>(rfc_sample).is_ok());
180 }
181 }
182
183 mod verify {
184 use super::*;
185
186 #[test]
187 #[wasm_bindgen_test]
188 fn should_succeed_when_status_valid() {
189 let account = AcmeAccount {
190 status: AcmeAccountStatus::Valid,
191 ..Default::default()
192 };
193 assert!(account.verify().is_ok());
194 }
195
196 #[test]
197 #[wasm_bindgen_test]
198 fn should_fail_when_status_deactivated() {
199 let account = AcmeAccount {
200 status: AcmeAccountStatus::Deactivated,
201 ..Default::default()
202 };
203 assert!(matches!(
204 account.verify().unwrap_err(),
205 RustyAcmeError::AccountError(AcmeAccountError::Deactivated)
206 ));
207 }
208
209 #[test]
210 #[wasm_bindgen_test]
211 fn should_fail_when_status_revoked() {
212 let account = AcmeAccount {
213 status: AcmeAccountStatus::Revoked,
214 ..Default::default()
215 };
216 assert!(matches!(
217 account.verify().unwrap_err(),
218 RustyAcmeError::AccountError(AcmeAccountError::Revoked)
219 ));
220 }
221
222 #[test]
223 #[wasm_bindgen_test]
224 fn should_fail_when_orders_absent() {
225 let account = AcmeAccount {
226 orders: None,
227 ..Default::default()
228 };
229 assert!(matches!(
230 account.verify().unwrap_err(),
231 RustyAcmeError::SmallstepImplementationError("Newly created account should have 'orders' url")
232 ));
233 }
234 }
235
236 mod acct_url {
237 use super::*;
238
239 #[test]
240 #[wasm_bindgen_test]
241 fn should_trim_last_orders_segment() {
242 let base = "https://acme-server/acme/wire-acme/account/muYiJmuJRn9u2L0tdI5bu11T7QqqPR1u";
243 let orders_url = format!("{base}/orders");
244 let account = AcmeAccount {
245 orders: Some(orders_url.parse().unwrap()),
246 ..Default::default()
247 };
248 assert_eq!(account.acct_url().unwrap().as_str(), base);
249 }
250
251 #[test]
252 #[wasm_bindgen_test]
253 fn should_fail_when_orders_absent() {
254 let account = AcmeAccount {
255 orders: None,
256 ..Default::default()
257 };
258 assert!(matches!(
259 account.acct_url().unwrap_err(),
260 RustyAcmeError::SmallstepImplementationError("Account should have 'orders' url")
261 ));
262 }
263
264 #[test]
265 #[wasm_bindgen_test]
266 fn should_fail_when_orders_url_doesnt_end_with_orders() {
267 let base = "https://acme-server/acme/wire-acme/account/muYiJmuJRn9u2L0tdI5bu11T7QqqPR1u";
268 let orders_url = format!("{base}/error");
269 let account = AcmeAccount {
270 orders: Some(orders_url.parse().unwrap()),
271 ..Default::default()
272 };
273 assert!(matches!(
274 account.acct_url().unwrap_err(),
275 RustyAcmeError::SmallstepImplementationError("Invalid 'orders' URL in account")
276 ));
277 }
278 }
279}