]>
Commit | Line | Data |
---|---|---|
47af324d WB |
1 | //! ACME Account management and creation. The [`Account`] type also contains most of the ACME API |
2 | //! entry point helpers. | |
3 | ||
5aee14ac | 4 | use std::collections::HashMap; |
aa230682 WB |
5 | use std::convert::TryFrom; |
6 | ||
7 | use openssl::pkey::{PKey, Private}; | |
8 | use serde::{Deserialize, Serialize}; | |
9 | use serde_json::Value; | |
10 | ||
afc59f6d | 11 | use crate::authorization::{Authorization, GetAuthorization}; |
aa230682 WB |
12 | use crate::b64u; |
13 | use crate::directory::Directory; | |
88f7e190 | 14 | use crate::eab::ExternalAccountBinding; |
aa230682 | 15 | use crate::jws::Jws; |
88f7e190 | 16 | use crate::key::{Jwk, PublicKey}; |
afc59f6d | 17 | use crate::order::{NewOrder, Order, OrderData}; |
aa230682 WB |
18 | use crate::request::Request; |
19 | use crate::Error; | |
20 | ||
47af324d WB |
21 | /// An ACME Account. |
22 | /// | |
23 | /// This contains the location URL, the account data and the private key for an account. | |
24 | /// This can directly be serialized via serde to persist the account. | |
25 | /// | |
26 | /// In order to register a new account with an ACME provider, see the [`Account::creator`] method. | |
aa230682 WB |
27 | #[derive(Deserialize, Serialize)] |
28 | #[serde(rename_all = "camelCase")] | |
29 | pub struct Account { | |
30 | /// Account location URL. | |
31 | pub location: String, | |
32 | ||
33 | /// Acme account data. | |
34 | pub data: AccountData, | |
35 | ||
36 | /// base64url encoded PEM formatted private key. | |
37 | pub private_key: String, | |
38 | } | |
39 | ||
40 | impl Account { | |
47af324d | 41 | /// Rebuild an account from its components. |
aa230682 WB |
42 | pub fn from_parts(location: String, private_key: String, data: AccountData) -> Self { |
43 | Self { | |
44 | location, | |
aa230682 | 45 | data, |
1d1f80f5 | 46 | private_key, |
aa230682 WB |
47 | } |
48 | } | |
49 | ||
47af324d WB |
50 | /// Builds an [`AccountCreator`]. This handles creation of the private key and account data as |
51 | /// well as handling the response sent by the server for the registration request. | |
aa230682 WB |
52 | pub fn creator() -> AccountCreator { |
53 | AccountCreator::default() | |
54 | } | |
55 | ||
47af324d WB |
56 | /// Place a new order. This will build a [`NewOrder`] representing an in flight order creation |
57 | /// request. | |
58 | /// | |
aa230682 WB |
59 | /// The returned `NewOrder`'s `request` option is *guaranteed* to be `Some(Request)`. |
60 | pub fn new_order( | |
61 | &self, | |
62 | order: &OrderData, | |
63 | directory: &Directory, | |
64 | nonce: &str, | |
65 | ) -> Result<NewOrder, Error> { | |
66 | let key = PKey::private_key_from_pem(self.private_key.as_bytes())?; | |
67 | ||
68 | if order.identifiers.is_empty() { | |
69 | return Err(Error::EmptyOrder); | |
70 | } | |
71 | ||
72 | let url = directory.new_order_url(); | |
73 | let body = serde_json::to_string(&Jws::new( | |
74 | &key, | |
75 | Some(self.location.clone()), | |
76 | url.to_owned(), | |
77 | nonce.to_owned(), | |
78 | order, | |
79 | )?)?; | |
80 | ||
81 | let request = Request { | |
82 | url: url.to_owned(), | |
83 | method: "POST", | |
84 | content_type: crate::request::JSON_CONTENT_TYPE, | |
85 | body, | |
86 | expected: crate::request::CREATED, | |
87 | }; | |
88 | ||
89 | Ok(NewOrder::new(request)) | |
90 | } | |
91 | ||
47af324d | 92 | /// Prepare a "POST-as-GET" request to fetch data. Low level helper. |
aa230682 WB |
93 | pub fn get_request(&self, url: &str, nonce: &str) -> Result<Request, Error> { |
94 | let key = PKey::private_key_from_pem(self.private_key.as_bytes())?; | |
95 | let body = serde_json::to_string(&Jws::new_full( | |
96 | &key, | |
97 | Some(self.location.clone()), | |
98 | url.to_owned(), | |
99 | nonce.to_owned(), | |
100 | String::new(), | |
101 | )?)?; | |
102 | ||
103 | Ok(Request { | |
104 | url: url.to_owned(), | |
105 | method: "POST", | |
106 | content_type: crate::request::JSON_CONTENT_TYPE, | |
107 | body, | |
108 | expected: 200, | |
109 | }) | |
110 | } | |
111 | ||
47af324d | 112 | /// Prepare a JSON POST request. Low level helper. |
aa230682 WB |
113 | pub fn post_request<T: Serialize>( |
114 | &self, | |
115 | url: &str, | |
116 | nonce: &str, | |
117 | data: &T, | |
118 | ) -> Result<Request, Error> { | |
119 | let key = PKey::private_key_from_pem(self.private_key.as_bytes())?; | |
120 | let body = serde_json::to_string(&Jws::new( | |
121 | &key, | |
122 | Some(self.location.clone()), | |
123 | url.to_owned(), | |
124 | nonce.to_owned(), | |
125 | data, | |
126 | )?)?; | |
127 | ||
128 | Ok(Request { | |
129 | url: url.to_owned(), | |
130 | method: "POST", | |
131 | content_type: crate::request::JSON_CONTENT_TYPE, | |
132 | body, | |
133 | expected: 200, | |
134 | }) | |
135 | } | |
136 | ||
afc59f6d WB |
137 | /// Prepare a JSON POST request. |
138 | fn post_request_raw_payload( | |
139 | &self, | |
140 | url: &str, | |
141 | nonce: &str, | |
142 | payload: String, | |
143 | ) -> Result<Request, Error> { | |
144 | let key = PKey::private_key_from_pem(self.private_key.as_bytes())?; | |
145 | let body = serde_json::to_string(&Jws::new_full( | |
146 | &key, | |
147 | Some(self.location.clone()), | |
148 | url.to_owned(), | |
149 | nonce.to_owned(), | |
150 | payload, | |
151 | )?)?; | |
152 | ||
153 | Ok(Request { | |
154 | url: url.to_owned(), | |
155 | method: "POST", | |
156 | content_type: crate::request::JSON_CONTENT_TYPE, | |
157 | body, | |
158 | expected: 200, | |
159 | }) | |
160 | } | |
161 | ||
aa230682 WB |
162 | /// Get the "key authorization" for a token. |
163 | pub fn key_authorization(&self, token: &str) -> Result<String, Error> { | |
164 | let key = PKey::private_key_from_pem(self.private_key.as_bytes())?; | |
165 | let thumbprint = PublicKey::try_from(&*key)?.thumbprint()?; | |
166 | Ok(format!("{}.{}", token, thumbprint)) | |
167 | } | |
168 | ||
169 | /// Get the TXT field value for a dns-01 token. This is the base64url encoded sha256 digest of | |
170 | /// the key authorization value. | |
171 | pub fn dns_01_txt_value(&self, token: &str) -> Result<String, Error> { | |
172 | let key_authorization = self.key_authorization(token)?; | |
173 | let digest = openssl::sha::sha256(key_authorization.as_bytes()); | |
174 | Ok(b64u::encode(&digest)) | |
175 | } | |
afc59f6d WB |
176 | |
177 | /// Prepare a request to update account data. | |
178 | /// | |
179 | /// This is a rather low level interface. You should know what you're doing. | |
180 | pub fn update_account_request<T: Serialize>( | |
181 | &self, | |
182 | nonce: &str, | |
183 | data: &T, | |
184 | ) -> Result<Request, Error> { | |
185 | self.post_request(&self.location, nonce, data) | |
186 | } | |
187 | ||
188 | /// Prepare a request to deactivate this account. | |
afc59f6d WB |
189 | pub fn deactivate_account_request<T: Serialize>(&self, nonce: &str) -> Result<Request, Error> { |
190 | self.post_request_raw_payload( | |
191 | &self.location, | |
192 | nonce, | |
193 | r#"{"status":"deactivated"}"#.to_string(), | |
194 | ) | |
195 | } | |
196 | ||
197 | /// Prepare a request to query an Authorization for an Order. | |
198 | /// | |
199 | /// Returns `Ok(None)` if `auth_index` is out of out of range. You can query the number of | |
200 | /// authorizations from via [`Order::authorization_len`] or by manually inspecting its | |
201 | /// `.data.authorization` vector. | |
202 | pub fn get_authorization( | |
203 | &self, | |
204 | order: &Order, | |
205 | auth_index: usize, | |
206 | nonce: &str, | |
207 | ) -> Result<Option<GetAuthorization>, Error> { | |
208 | match order.authorization(auth_index) { | |
209 | None => Ok(None), | |
210 | Some(url) => Ok(Some(GetAuthorization::new(self.get_request(url, nonce)?))), | |
211 | } | |
212 | } | |
213 | ||
214 | /// Prepare a request to validate a Challenge from an Authorization. | |
215 | /// | |
7bd0bfe1 WB |
216 | /// Returns `Ok(None)` if `challenge_index` is out of out of range. The challenge count is |
217 | /// available by inspecting the [`Authorization::challenges`] vector. | |
afc59f6d WB |
218 | /// |
219 | /// This returns a raw `Request` since validation takes some time and the `Authorization` | |
220 | /// object has to be re-queried and its `status` inspected. | |
221 | pub fn validate_challenge( | |
222 | &self, | |
223 | authorization: &Authorization, | |
224 | challenge_index: usize, | |
225 | nonce: &str, | |
226 | ) -> Result<Option<Request>, Error> { | |
227 | match authorization.challenges.get(challenge_index) { | |
228 | None => Ok(None), | |
229 | Some(challenge) => self | |
230 | .post_request_raw_payload(&challenge.url, nonce, "{}".to_string()) | |
231 | .map(Some), | |
232 | } | |
233 | } | |
558f51a1 WB |
234 | |
235 | /// Prepare a request to revoke a certificate. | |
236 | /// | |
237 | /// The certificate can be either PEM or DER formatted. | |
238 | /// | |
239 | /// Note that this uses the account's key for authorization. | |
240 | /// | |
241 | /// Revocation using a certificate's private key is not yet implemented. | |
242 | pub fn revoke_certificate( | |
243 | &self, | |
244 | certificate: &[u8], | |
245 | reason: Option<u32>, | |
246 | ) -> Result<CertificateRevocation, Error> { | |
247 | let cert = if certificate.starts_with(b"-----BEGIN CERTIFICATE-----") { | |
248 | b64u::encode(&openssl::x509::X509::from_pem(certificate)?.to_der()?) | |
249 | } else { | |
250 | b64u::encode(certificate) | |
251 | }; | |
252 | ||
253 | let data = match reason { | |
254 | Some(reason) => serde_json::json!({ "certificate": cert, "reason": reason }), | |
255 | None => serde_json::json!({ "certificate": cert }), | |
256 | }; | |
257 | ||
258 | Ok(CertificateRevocation { | |
259 | account: self, | |
260 | data, | |
261 | }) | |
262 | } | |
263 | } | |
264 | ||
265 | /// Certificate revocation involves converting the certificate to base64url encoded DER and then | |
266 | /// embedding it in a json structure. Since we also need a nonce and possibly retry the request if | |
267 | /// a `BadNonce` error happens, this caches the converted data for efficiency. | |
268 | pub struct CertificateRevocation<'a> { | |
269 | account: &'a Account, | |
270 | data: Value, | |
271 | } | |
272 | ||
273 | impl CertificateRevocation<'_> { | |
47af324d | 274 | /// Create the revocation request using the specified nonce for the given directory. |
558f51a1 | 275 | pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> { |
f406e6fb WB |
276 | self.account |
277 | .post_request(&directory.data.revoke_cert, nonce, &self.data) | |
558f51a1 | 278 | } |
aa230682 WB |
279 | } |
280 | ||
47af324d | 281 | /// Status of an ACME account. |
619414d4 | 282 | #[cfg_attr(feature="api-types", proxmox_schema::api())] |
aa230682 WB |
283 | #[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] |
284 | #[serde(rename_all = "camelCase")] | |
285 | pub enum AccountStatus { | |
47af324d WB |
286 | /// This is not part of the ACME API, but a temporary marker for us until the ACME provider |
287 | /// tells us the account's real status. | |
aa230682 WB |
288 | #[serde(rename = "<invalid>")] |
289 | New, | |
47af324d WB |
290 | |
291 | /// Means the account is valid and can be used. | |
aa230682 | 292 | Valid, |
47af324d WB |
293 | |
294 | /// The account has been deactivated by its user and cannot be used anymore. | |
aa230682 | 295 | Deactivated, |
47af324d WB |
296 | |
297 | /// The account has been revoked by the server and cannot be used anymore. | |
aa230682 WB |
298 | Revoked, |
299 | } | |
300 | ||
301 | impl AccountStatus { | |
302 | #[inline] | |
303 | fn new() -> Self { | |
304 | AccountStatus::New | |
305 | } | |
306 | ||
307 | #[inline] | |
308 | fn is_new(&self) -> bool { | |
309 | *self == AccountStatus::New | |
310 | } | |
311 | } | |
312 | ||
619414d4 DM |
313 | #[cfg_attr(feature="api-types", proxmox_schema::api( |
314 | properties: { | |
315 | extra: { type: Object, properties: {}, additional_properties: true }, | |
316 | contact: { type: Array, items: { type: String, description: "Contact Info." }} | |
317 | } | |
318 | ))] | |
47af324d WB |
319 | /// ACME Account data. This is the part of the account returned from and possibly sent to the ACME |
320 | /// provider. Some fields may be uptdated by the user via a request to the account location, others | |
321 | /// may not be changed. | |
03707232 | 322 | #[derive(Clone, PartialEq, Deserialize, Serialize)] |
aa230682 WB |
323 | #[serde(rename_all = "camelCase")] |
324 | pub struct AccountData { | |
47af324d | 325 | /// The current account status. |
aa230682 WB |
326 | #[serde( |
327 | skip_serializing_if = "AccountStatus::is_new", | |
328 | default = "AccountStatus::new" | |
329 | )] | |
cbfeb58c | 330 | pub status: AccountStatus, |
aa230682 | 331 | |
47af324d | 332 | /// URLs to currently pending orders. |
aa230682 | 333 | #[serde(skip_serializing_if = "Option::is_none")] |
cbfeb58c | 334 | pub orders: Option<String>, |
aa230682 | 335 | |
47af324d WB |
336 | /// The acccount's contact info. |
337 | /// | |
338 | /// This usually contains a `"mailto:<email address>"` entry but may also contain some other | |
339 | /// data if the server accepts it. | |
aa230682 | 340 | #[serde(skip_serializing_if = "Vec::is_empty", default)] |
cbfeb58c | 341 | pub contact: Vec<String>, |
aa230682 | 342 | |
47af324d | 343 | /// Indicated whether the user agreed to the ACME provider's terms of service. |
aa230682 | 344 | #[serde(skip_serializing_if = "Option::is_none")] |
cbfeb58c | 345 | pub terms_of_service_agreed: Option<bool>, |
aa230682 | 346 | |
88f7e190 | 347 | /// External account information. |
aa230682 | 348 | #[serde(skip_serializing_if = "Option::is_none")] |
88f7e190 | 349 | pub external_account_binding: Option<ExternalAccountBinding>, |
aa230682 | 350 | |
47af324d | 351 | /// This is only used by the client when querying an account. |
aa230682 | 352 | #[serde(default = "default_true", skip_serializing_if = "is_false")] |
cbfeb58c | 353 | pub only_return_existing: bool, |
5aee14ac | 354 | |
47af324d | 355 | /// Stores unknown fields if there are any. |
5aee14ac WB |
356 | #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")] |
357 | pub extra: HashMap<String, Value>, | |
aa230682 WB |
358 | } |
359 | ||
360 | #[inline] | |
361 | fn default_true() -> bool { | |
362 | true | |
363 | } | |
364 | ||
365 | #[inline] | |
366 | fn is_false(b: &bool) -> bool { | |
367 | !*b | |
368 | } | |
369 | ||
47af324d WB |
370 | /// Helper to create an account. |
371 | /// | |
372 | /// This is used to generate a private key and set the contact info for the account. Afterwards the | |
7bd0bfe1 WB |
373 | /// creation request can be created via the [`request`](AccountCreator::request()) method, giving |
374 | /// it a nonce and a directory. This can be repeated, if necessary, like when the nonce fails. | |
47af324d | 375 | /// |
7bd0bfe1 WB |
376 | /// When the server sends a succesful response, it should be passed to the |
377 | /// [`response`](AccountCreator::response()) method to finish the creation of an [`Account`] which | |
378 | /// can then be persisted. | |
aa230682 WB |
379 | #[derive(Default)] |
380 | #[must_use = "when creating an account you must pass the response to AccountCreator::response()!"] | |
381 | pub struct AccountCreator { | |
382 | contact: Vec<String>, | |
383 | terms_of_service_agreed: bool, | |
384 | key: Option<PKey<Private>>, | |
88f7e190 | 385 | eab_credentials: Option<(String, PKey<Private>)>, |
aa230682 WB |
386 | } |
387 | ||
388 | impl AccountCreator { | |
389 | /// Replace the contact infor with the provided ACME compatible data. | |
390 | pub fn set_contacts(mut self, contact: Vec<String>) -> Self { | |
391 | self.contact = contact; | |
392 | self | |
393 | } | |
394 | ||
395 | /// Append a contact string. | |
396 | pub fn contact(mut self, contact: String) -> Self { | |
397 | self.contact.push(contact); | |
398 | self | |
399 | } | |
400 | ||
401 | /// Append an email address to the contact list. | |
402 | pub fn email(self, email: String) -> Self { | |
403 | self.contact(format!("mailto:{}", email)) | |
404 | } | |
405 | ||
406 | /// Change whether the account agrees to the terms of service. Use the directory's or client's | |
407 | /// `terms_of_service_url()` method to present the user with the Terms of Service. | |
408 | pub fn agree_to_tos(mut self, agree: bool) -> Self { | |
409 | self.terms_of_service_agreed = agree; | |
410 | self | |
411 | } | |
412 | ||
88f7e190 FG |
413 | /// Set the EAB credentials for the account registration |
414 | pub fn set_eab_credentials(mut self, kid: String, hmac_key: String) -> Result<Self, Error> { | |
415 | let hmac_key = PKey::hmac(&base64::decode(hmac_key)?)?; | |
416 | self.eab_credentials = Some((kid, hmac_key)); | |
417 | Ok(self) | |
418 | } | |
419 | ||
aa230682 WB |
420 | /// Generate a new RSA key of the specified key size. |
421 | pub fn generate_rsa_key(self, bits: u32) -> Result<Self, Error> { | |
422 | let key = openssl::rsa::Rsa::generate(bits)?; | |
423 | Ok(self.with_key(PKey::from_rsa(key)?)) | |
424 | } | |
425 | ||
426 | /// Generate a new P-256 EC key. | |
427 | pub fn generate_ec_key(self) -> Result<Self, Error> { | |
428 | let key = openssl::ec::EcKey::generate( | |
429 | openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?.as_ref(), | |
430 | )?; | |
431 | Ok(self.with_key(PKey::from_ec_key(key)?)) | |
432 | } | |
433 | ||
434 | /// Use an existing key. Note that only RSA and EC keys using the `P-256` curve are currently | |
435 | /// supported, however, this will not be checked at this point. | |
436 | pub fn with_key(mut self, key: PKey<Private>) -> Self { | |
437 | self.key = Some(key); | |
438 | self | |
439 | } | |
440 | ||
441 | /// Prepare a HTTP request to create this account. | |
442 | /// | |
443 | /// Changes to the user data made after this will have no effect on the account generated with | |
444 | /// the resulting request. | |
445 | /// Changing the private key between using the request and passing the response to | |
7bd0bfe1 | 446 | /// [`response`](AccountCreator::response()) will render the account unusable! |
aa230682 | 447 | pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> { |
95381262 | 448 | let key = self.key.as_deref().ok_or(Error::MissingKey)?; |
88f7e190 FG |
449 | let url = directory.new_account_url(); |
450 | ||
451 | let external_account_binding = self | |
452 | .eab_credentials | |
453 | .as_ref() | |
454 | .map(|cred| { | |
455 | ExternalAccountBinding::new(&cred.0, &cred.1, Jwk::try_from(key)?, url.to_string()) | |
456 | }) | |
457 | .transpose()?; | |
aa230682 WB |
458 | |
459 | let data = AccountData { | |
460 | orders: None, | |
461 | status: AccountStatus::New, | |
462 | contact: self.contact.clone(), | |
463 | terms_of_service_agreed: if self.terms_of_service_agreed { | |
464 | Some(true) | |
465 | } else { | |
466 | None | |
467 | }, | |
88f7e190 | 468 | external_account_binding, |
aa230682 | 469 | only_return_existing: false, |
5aee14ac | 470 | extra: HashMap::new(), |
aa230682 WB |
471 | }; |
472 | ||
aa230682 WB |
473 | let body = serde_json::to_string(&Jws::new( |
474 | key, | |
475 | None, | |
476 | url.to_owned(), | |
477 | nonce.to_owned(), | |
478 | &data, | |
479 | )?)?; | |
480 | ||
481 | Ok(Request { | |
482 | url: url.to_owned(), | |
483 | method: "POST", | |
484 | content_type: crate::request::JSON_CONTENT_TYPE, | |
485 | body, | |
486 | expected: crate::request::CREATED, | |
487 | }) | |
488 | } | |
489 | ||
7bd0bfe1 WB |
490 | /// After issuing the request from [`request()`](AccountCreator::request()), the response's |
491 | /// `Location` header and body must be passed to this for verification and to create an account | |
492 | /// which is to be persisted! | |
aa230682 WB |
493 | pub fn response(self, location_header: String, response_body: &[u8]) -> Result<Account, Error> { |
494 | let private_key = self | |
495 | .key | |
496 | .ok_or(Error::MissingKey)? | |
497 | .private_key_to_pem_pkcs8()?; | |
498 | let private_key = String::from_utf8(private_key).map_err(|_| { | |
95381262 | 499 | Error::Custom("PEM key contained illegal non-utf-8 characters".to_string()) |
aa230682 WB |
500 | })?; |
501 | ||
502 | Ok(Account { | |
503 | location: location_header, | |
504 | data: serde_json::from_slice(response_body) | |
505 | .map_err(|err| Error::BadAccountData(err.to_string()))?, | |
506 | private_key, | |
507 | }) | |
508 | } | |
509 | } |