]> git.proxmox.com Git - proxmox-backup.git/blob - src/config/tfa.rs
update to first proxmox crate split
[proxmox-backup.git] / src / config / tfa.rs
1 use std::collections::HashMap;
2 use std::fs::File;
3 use std::io::{self, Read, Seek, SeekFrom};
4 use std::os::unix::fs::OpenOptionsExt;
5 use std::os::unix::io::AsRawFd;
6 use std::path::PathBuf;
7
8 use anyhow::{bail, format_err, Error};
9 use nix::sys::stat::Mode;
10 use openssl::hash::MessageDigest;
11 use openssl::pkey::PKey;
12 use openssl::sign::Signer;
13 use serde::{de::Deserializer, Deserialize, Serialize};
14 use serde_json::Value;
15 use webauthn_rs::{proto::UserVerificationPolicy, Webauthn};
16
17 use webauthn_rs::proto::Credential as WebauthnCredential;
18
19 use proxmox::sys::error::SysError;
20 use proxmox::tools::fs::CreateOptions;
21 use proxmox_schema::{api, Updater};
22 use proxmox_tfa::{totp::Totp, u2f};
23 use proxmox_uuid::Uuid;
24
25 use pbs_buildcfg::configdir;
26 use pbs_config::{open_backup_lockfile, BackupLockGuard};
27 use pbs_api_types::{Userid, User};
28
29 /// Mapping of userid to TFA entry.
30 pub type TfaUsers = HashMap<Userid, TfaUserData>;
31
32 const CONF_FILE: &str = configdir!("/tfa.json");
33 const LOCK_FILE: &str = configdir!("/tfa.json.lock");
34
35 const CHALLENGE_DATA_PATH: &str = pbs_buildcfg::rundir!("/tfa/challenges");
36
37 /// U2F registration challenges time out after 2 minutes.
38 const CHALLENGE_TIMEOUT: i64 = 2 * 60;
39
40 pub fn read_lock() -> Result<BackupLockGuard, Error> {
41 open_backup_lockfile(LOCK_FILE, None, false)
42 }
43
44 pub fn write_lock() -> Result<BackupLockGuard, Error> {
45 open_backup_lockfile(LOCK_FILE, None, true)
46 }
47
48 /// Read the TFA entries.
49 pub fn read() -> Result<TfaConfig, Error> {
50 let file = match File::open(CONF_FILE) {
51 Ok(file) => file,
52 Err(ref err) if err.not_found() => return Ok(TfaConfig::default()),
53 Err(err) => return Err(err.into()),
54 };
55
56 Ok(serde_json::from_reader(file)?)
57 }
58
59 /// Get the webauthn config with a digest.
60 ///
61 /// This is meant only for configuration updates, which currently only means webauthn updates.
62 /// Since this is meant to be done only once (since changes will lock out users), this should be
63 /// used rarely, since the digest calculation is currently a bit more involved.
64 pub fn webauthn_config() -> Result<Option<(WebauthnConfig, [u8; 32])>, Error>{
65 Ok(match read()?.webauthn {
66 Some(wa) => {
67 let digest = wa.digest()?;
68 Some((wa, digest))
69 }
70 None => None,
71 })
72 }
73
74 /// Requires the write lock to be held.
75 pub fn write(data: &TfaConfig) -> Result<(), Error> {
76 let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
77
78 let json = serde_json::to_vec(data)?;
79 proxmox::tools::fs::replace_file(CONF_FILE, &json, options)
80 }
81
82 #[derive(Deserialize, Serialize)]
83 pub struct U2fConfig {
84 appid: String,
85 }
86
87 #[api]
88 #[derive(Clone, Deserialize, Serialize, Updater)]
89 #[serde(deny_unknown_fields)]
90 /// Server side webauthn server configuration.
91 pub struct WebauthnConfig {
92 /// Relying party name. Any text identifier.
93 ///
94 /// Changing this *may* break existing credentials.
95 pub rp: String,
96
97 /// Site origin. Must be a `https://` URL (or `http://localhost`). Should contain the address
98 /// users type in their browsers to access the web interface.
99 ///
100 /// Changing this *may* break existing credentials.
101 pub origin: String,
102
103 /// Relying part ID. Must be the domain name without protocol, port or location.
104 ///
105 /// Changing this *will* break existing credentials.
106 pub id: String,
107 }
108
109 impl WebauthnConfig {
110 pub fn digest(&self) -> Result<[u8; 32], Error> {
111 let digest_data = pbs_tools::json::to_canonical_json(&serde_json::to_value(self)?)?;
112 Ok(openssl::sha::sha256(&digest_data))
113 }
114 }
115
116 /// For now we just implement this on the configuration this way.
117 ///
118 /// Note that we may consider changing this so `get_origin` returns the `Host:` header provided by
119 /// the connecting client.
120 impl webauthn_rs::WebauthnConfig for WebauthnConfig {
121 fn get_relying_party_name(&self) -> String {
122 self.rp.clone()
123 }
124
125 fn get_origin(&self) -> &String {
126 &self.origin
127 }
128
129 fn get_relying_party_id(&self) -> String {
130 self.id.clone()
131 }
132 }
133
134 /// Helper to get a u2f instance from a u2f config, or `None` if there isn't one configured.
135 fn get_u2f(u2f: &Option<U2fConfig>) -> Option<u2f::U2f> {
136 u2f.as_ref()
137 .map(|cfg| u2f::U2f::new(cfg.appid.clone(), cfg.appid.clone()))
138 }
139
140 /// Helper to get a u2f instance from a u2f config.
141 ///
142 /// This is outside of `TfaConfig` to not borrow its `&self`.
143 fn check_u2f(u2f: &Option<U2fConfig>) -> Result<u2f::U2f, Error> {
144 get_u2f(u2f).ok_or_else(|| format_err!("no u2f configuration available"))
145 }
146
147 /// Helper to get a `Webauthn` instance from a `WebauthnConfig`, or `None` if there isn't one
148 /// configured.
149 fn get_webauthn(waconfig: &Option<WebauthnConfig>) -> Option<Webauthn<WebauthnConfig>> {
150 waconfig.clone().map(Webauthn::new)
151 }
152
153 /// Helper to get a u2f instance from a u2f config.
154 ///
155 /// This is outside of `TfaConfig` to not borrow its `&self`.
156 fn check_webauthn(waconfig: &Option<WebauthnConfig>) -> Result<Webauthn<WebauthnConfig>, Error> {
157 get_webauthn(waconfig).ok_or_else(|| format_err!("no webauthn configuration available"))
158 }
159
160 /// TFA Configuration for this instance.
161 #[derive(Default, Deserialize, Serialize)]
162 pub struct TfaConfig {
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub u2f: Option<U2fConfig>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub webauthn: Option<WebauthnConfig>,
168
169 #[serde(skip_serializing_if = "TfaUsers::is_empty", default)]
170 pub users: TfaUsers,
171 }
172
173 impl TfaConfig {
174 /// Get a two factor authentication challenge for a user, if the user has TFA set up.
175 pub fn login_challenge(&mut self, userid: &Userid) -> Result<Option<TfaChallenge>, Error> {
176 match self.users.get_mut(userid) {
177 Some(udata) => udata.challenge(
178 userid,
179 get_webauthn(&self.webauthn),
180 get_u2f(&self.u2f).as_ref(),
181 ),
182 None => Ok(None),
183 }
184 }
185
186 /// Get a u2f registration challenge.
187 fn u2f_registration_challenge(
188 &mut self,
189 userid: &Userid,
190 description: String,
191 ) -> Result<String, Error> {
192 let u2f = check_u2f(&self.u2f)?;
193
194 self.users
195 .entry(userid.clone())
196 .or_default()
197 .u2f_registration_challenge(userid, &u2f, description)
198 }
199
200 /// Finish a u2f registration challenge.
201 fn u2f_registration_finish(
202 &mut self,
203 userid: &Userid,
204 challenge: &str,
205 response: &str,
206 ) -> Result<String, Error> {
207 let u2f = check_u2f(&self.u2f)?;
208
209 match self.users.get_mut(userid) {
210 Some(user) => user.u2f_registration_finish(userid, &u2f, challenge, response),
211 None => bail!("no such challenge"),
212 }
213 }
214
215 /// Get a webauthn registration challenge.
216 fn webauthn_registration_challenge(
217 &mut self,
218 user: &Userid,
219 description: String,
220 ) -> Result<String, Error> {
221 let webauthn = check_webauthn(&self.webauthn)?;
222
223 self.users
224 .entry(user.clone())
225 .or_default()
226 .webauthn_registration_challenge(webauthn, user, description)
227 }
228
229 /// Finish a webauthn registration challenge.
230 fn webauthn_registration_finish(
231 &mut self,
232 userid: &Userid,
233 challenge: &str,
234 response: &str,
235 ) -> Result<String, Error> {
236 let webauthn = check_webauthn(&self.webauthn)?;
237
238 let response: webauthn_rs::proto::RegisterPublicKeyCredential =
239 serde_json::from_str(response)
240 .map_err(|err| format_err!("error parsing challenge response: {}", err))?;
241
242 match self.users.get_mut(userid) {
243 Some(user) => user.webauthn_registration_finish(webauthn, userid, challenge, response),
244 None => bail!("no such challenge"),
245 }
246 }
247
248 /// Verify a TFA response.
249 fn verify(
250 &mut self,
251 userid: &Userid,
252 challenge: &TfaChallenge,
253 response: TfaResponse,
254 ) -> Result<(), Error> {
255 match self.users.get_mut(userid) {
256 Some(user) => match response {
257 TfaResponse::Totp(value) => user.verify_totp(&value),
258 TfaResponse::U2f(value) => match &challenge.u2f {
259 Some(challenge) => {
260 let u2f = check_u2f(&self.u2f)?;
261 user.verify_u2f(u2f, &challenge.challenge, value)
262 }
263 None => bail!("no u2f factor available for user '{}'", userid),
264 },
265 TfaResponse::Webauthn(value) => {
266 let webauthn = check_webauthn(&self.webauthn)?;
267 user.verify_webauthn(userid, webauthn, value)
268 }
269 TfaResponse::Recovery(value) => user.verify_recovery(&value),
270 },
271 None => bail!("no 2nd factor available for user '{}'", userid),
272 }
273 }
274
275 /// Remove non-existent users.
276 pub fn cleanup_users(&mut self, config: &proxmox_section_config::SectionConfigData) {
277 self.users
278 .retain(|user, _| config.lookup::<User>("user", user.as_str()).is_ok());
279 }
280
281 /// Remove a user. Returns `true` if the user actually existed.
282 pub fn remove_user(&mut self, user: &Userid) -> bool {
283 self.users.remove(user).is_some()
284 }
285 }
286
287 #[api]
288 /// Over the API we only provide this part when querying a user's second factor list.
289 #[derive(Deserialize, Serialize)]
290 #[serde(deny_unknown_fields)]
291 pub struct TfaInfo {
292 /// The id used to reference this entry.
293 pub id: String,
294
295 /// User chosen description for this entry.
296 #[serde(skip_serializing_if = "String::is_empty")]
297 pub description: String,
298
299 /// Creation time of this entry as unix epoch.
300 pub created: i64,
301
302 /// Whether this TFA entry is currently enabled.
303 #[serde(skip_serializing_if = "is_default_tfa_enable")]
304 #[serde(default = "default_tfa_enable")]
305 pub enable: bool,
306 }
307
308 impl TfaInfo {
309 /// For recovery keys we have a fixed entry.
310 pub(crate) fn recovery(created: i64) -> Self {
311 Self {
312 id: "recovery".to_string(),
313 description: String::new(),
314 enable: true,
315 created,
316 }
317 }
318 }
319
320 /// A TFA entry for a user.
321 ///
322 /// This simply connects a raw registration to a non optional descriptive text chosen by the user.
323 #[derive(Deserialize, Serialize)]
324 #[serde(deny_unknown_fields)]
325 pub struct TfaEntry<T> {
326 #[serde(flatten)]
327 pub info: TfaInfo,
328
329 /// The actual entry.
330 entry: T,
331 }
332
333 impl<T> TfaEntry<T> {
334 /// Create an entry with a description. The id will be autogenerated.
335 fn new(description: String, entry: T) -> Self {
336 Self {
337 info: TfaInfo {
338 id: Uuid::generate().to_string(),
339 enable: true,
340 description,
341 created: proxmox_time::epoch_i64(),
342 },
343 entry,
344 }
345 }
346 }
347
348 trait IsExpired {
349 fn is_expired(&self, at_epoch: i64) -> bool;
350 }
351
352 /// A u2f registration challenge.
353 #[derive(Deserialize, Serialize)]
354 #[serde(deny_unknown_fields)]
355 pub struct U2fRegistrationChallenge {
356 /// JSON formatted challenge string.
357 challenge: String,
358
359 /// The description chosen by the user for this registration.
360 description: String,
361
362 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
363 created: i64,
364 }
365
366 impl U2fRegistrationChallenge {
367 pub fn new(challenge: String, description: String) -> Self {
368 Self {
369 challenge,
370 description,
371 created: proxmox_time::epoch_i64(),
372 }
373 }
374 }
375
376 impl IsExpired for U2fRegistrationChallenge {
377 fn is_expired(&self, at_epoch: i64) -> bool {
378 self.created < at_epoch
379 }
380 }
381
382 /// A webauthn registration challenge.
383 #[derive(Deserialize, Serialize)]
384 #[serde(deny_unknown_fields)]
385 pub struct WebauthnRegistrationChallenge {
386 /// Server side registration state data.
387 state: webauthn_rs::RegistrationState,
388
389 /// While this is basically the content of a `RegistrationState`, the webauthn-rs crate doesn't
390 /// make this public.
391 challenge: String,
392
393 /// The description chosen by the user for this registration.
394 description: String,
395
396 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
397 created: i64,
398 }
399
400 impl WebauthnRegistrationChallenge {
401 pub fn new(
402 state: webauthn_rs::RegistrationState,
403 challenge: String,
404 description: String,
405 ) -> Self {
406 Self {
407 state,
408 challenge,
409 description,
410 created: proxmox_time::epoch_i64(),
411 }
412 }
413 }
414
415 impl IsExpired for WebauthnRegistrationChallenge {
416 fn is_expired(&self, at_epoch: i64) -> bool {
417 self.created < at_epoch
418 }
419 }
420
421 /// A webauthn authentication challenge.
422 #[derive(Deserialize, Serialize)]
423 #[serde(deny_unknown_fields)]
424 pub struct WebauthnAuthChallenge {
425 /// Server side authentication state.
426 state: webauthn_rs::AuthenticationState,
427
428 /// While this is basically the content of a `AuthenticationState`, the webauthn-rs crate
429 /// doesn't make this public.
430 challenge: String,
431
432 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
433 created: i64,
434 }
435
436 impl WebauthnAuthChallenge {
437 pub fn new(state: webauthn_rs::AuthenticationState, challenge: String) -> Self {
438 Self {
439 state,
440 challenge,
441 created: proxmox_time::epoch_i64(),
442 }
443 }
444 }
445
446 impl IsExpired for WebauthnAuthChallenge {
447 fn is_expired(&self, at_epoch: i64) -> bool {
448 self.created < at_epoch
449 }
450 }
451
452 /// Active TFA challenges per user, stored in `CHALLENGE_DATA_PATH`.
453 #[derive(Default, Deserialize, Serialize)]
454 pub struct TfaUserChallenges {
455 /// Active u2f registration challenges for a user.
456 ///
457 /// Expired values are automatically filtered out while parsing the tfa configuration file.
458 #[serde(skip_serializing_if = "Vec::is_empty", default)]
459 #[serde(deserialize_with = "filter_expired_challenge")]
460 u2f_registrations: Vec<U2fRegistrationChallenge>,
461
462 /// Active webauthn registration challenges for a user.
463 ///
464 /// Expired values are automatically filtered out while parsing the tfa configuration file.
465 #[serde(skip_serializing_if = "Vec::is_empty", default)]
466 #[serde(deserialize_with = "filter_expired_challenge")]
467 webauthn_registrations: Vec<WebauthnRegistrationChallenge>,
468
469 /// Active webauthn registration challenges for a user.
470 ///
471 /// Expired values are automatically filtered out while parsing the tfa configuration file.
472 #[serde(skip_serializing_if = "Vec::is_empty", default)]
473 #[serde(deserialize_with = "filter_expired_challenge")]
474 webauthn_auths: Vec<WebauthnAuthChallenge>,
475 }
476
477 /// Container of `TfaUserChallenges` with the corresponding file lock guard.
478 ///
479 /// TODO: Implement a general file lock guarded struct container in the `proxmox` crate.
480 pub struct TfaUserChallengeData {
481 inner: TfaUserChallenges,
482 path: PathBuf,
483 lock: File,
484 }
485
486 impl TfaUserChallengeData {
487 /// Build the path to the challenge data file for a user.
488 fn challenge_data_path(userid: &Userid) -> PathBuf {
489 PathBuf::from(format!("{}/{}", CHALLENGE_DATA_PATH, userid))
490 }
491
492 /// Load the user's current challenges with the intent to create a challenge (create the file
493 /// if it does not exist), and keep a lock on the file.
494 fn open(userid: &Userid) -> Result<Self, Error> {
495 crate::server::create_run_dir()?;
496 let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
497 proxmox::tools::fs::create_path(CHALLENGE_DATA_PATH, Some(options.clone()), Some(options))
498 .map_err(|err| {
499 format_err!(
500 "failed to crate challenge data dir {:?}: {}",
501 CHALLENGE_DATA_PATH,
502 err
503 )
504 })?;
505
506 let path = Self::challenge_data_path(userid);
507
508 let mut file = std::fs::OpenOptions::new()
509 .create(true)
510 .read(true)
511 .write(true)
512 .truncate(false)
513 .mode(0o600)
514 .open(&path)
515 .map_err(|err| format_err!("failed to create challenge file {:?}: {}", path, err))?;
516
517 proxmox::tools::fs::lock_file(&mut file, true, None)?;
518
519 // the file may be empty, so read to a temporary buffer first:
520 let mut data = Vec::with_capacity(4096);
521
522 file.read_to_end(&mut data).map_err(|err| {
523 format_err!("failed to read challenge data for user {}: {}", userid, err)
524 })?;
525
526 let inner = if data.is_empty() {
527 Default::default()
528 } else {
529 serde_json::from_slice(&data).map_err(|err| {
530 format_err!(
531 "failed to parse challenge data for user {}: {}",
532 userid,
533 err
534 )
535 })?
536 };
537
538 Ok(Self {
539 inner,
540 path,
541 lock: file,
542 })
543 }
544
545 /// `open` without creating the file if it doesn't exist, to finish WA authentications.
546 fn open_no_create(userid: &Userid) -> Result<Option<Self>, Error> {
547 let path = Self::challenge_data_path(userid);
548 let mut file = match std::fs::OpenOptions::new()
549 .read(true)
550 .write(true)
551 .truncate(false)
552 .mode(0o600)
553 .open(&path)
554 {
555 Ok(file) => file,
556 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
557 Err(err) => return Err(err.into()),
558 };
559
560 proxmox::tools::fs::lock_file(&mut file, true, None)?;
561
562 let inner = serde_json::from_reader(&mut file).map_err(|err| {
563 format_err!("failed to read challenge data for user {}: {}", userid, err)
564 })?;
565
566 Ok(Some(Self {
567 inner,
568 path,
569 lock: file,
570 }))
571 }
572
573 /// Rewind & truncate the file for an update.
574 fn rewind(&mut self) -> Result<(), Error> {
575 let pos = self.lock.seek(SeekFrom::Start(0))?;
576 if pos != 0 {
577 bail!(
578 "unexpected result trying to rewind file, position is {}",
579 pos
580 );
581 }
582
583 proxmox::c_try!(unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) });
584
585 Ok(())
586 }
587
588 /// Save the current data. Note that we do not replace the file here since we lock the file
589 /// itself, as it is in `/run`, and the typical error case for this particular situation
590 /// (machine loses power) simply prevents some login, but that'll probably fail anyway for
591 /// other reasons then...
592 ///
593 /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
594 /// way also unlocks early.
595 fn save(mut self) -> Result<(), Error> {
596 self.rewind()?;
597
598 serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {
599 format_err!("failed to update challenge file {:?}: {}", self.path, err)
600 })?;
601
602 Ok(())
603 }
604
605 /// Finish a u2f registration. The challenge should correspond to an output of
606 /// `u2f_registration_challenge` (which is a stringified `RegistrationChallenge`). The response
607 /// should come directly from the client.
608 fn u2f_registration_finish(
609 &mut self,
610 u2f: &u2f::U2f,
611 challenge: &str,
612 response: &str,
613 ) -> Result<TfaEntry<u2f::Registration>, Error> {
614 let expire_before = proxmox_time::epoch_i64() - CHALLENGE_TIMEOUT;
615
616 let index = self
617 .inner
618 .u2f_registrations
619 .iter()
620 .position(|r| r.challenge == challenge)
621 .ok_or_else(|| format_err!("no such challenge"))?;
622
623 let reg = &self.inner.u2f_registrations[index];
624 if reg.is_expired(expire_before) {
625 bail!("no such challenge");
626 }
627
628 // the verify call only takes the actual challenge string, so we have to extract it
629 // (u2f::RegistrationChallenge did not always implement Deserialize...)
630 let chobj: Value = serde_json::from_str(challenge)
631 .map_err(|err| format_err!("error parsing original registration challenge: {}", err))?;
632 let challenge = chobj["challenge"]
633 .as_str()
634 .ok_or_else(|| format_err!("invalid registration challenge"))?;
635
636 let (mut reg, description) = match u2f.registration_verify(challenge, response)? {
637 None => bail!("verification failed"),
638 Some(reg) => {
639 let entry = self.inner.u2f_registrations.remove(index);
640 (reg, entry.description)
641 }
642 };
643
644 // we do not care about the attestation certificates, so don't store them
645 reg.certificate.clear();
646
647 Ok(TfaEntry::new(description, reg))
648 }
649
650 /// Finish a webauthn registration. The challenge should correspond to an output of
651 /// `webauthn_registration_challenge`. The response should come directly from the client.
652 fn webauthn_registration_finish(
653 &mut self,
654 webauthn: Webauthn<WebauthnConfig>,
655 challenge: &str,
656 response: webauthn_rs::proto::RegisterPublicKeyCredential,
657 existing_registrations: &[TfaEntry<WebauthnCredential>],
658 ) -> Result<TfaEntry<WebauthnCredential>, Error> {
659 let expire_before = proxmox_time::epoch_i64() - CHALLENGE_TIMEOUT;
660
661 let index = self
662 .inner
663 .webauthn_registrations
664 .iter()
665 .position(|r| r.challenge == challenge)
666 .ok_or_else(|| format_err!("no such challenge"))?;
667
668 let reg = self.inner.webauthn_registrations.remove(index);
669 if reg.is_expired(expire_before) {
670 bail!("no such challenge");
671 }
672
673 let credential =
674 webauthn.register_credential(response, reg.state, |id| -> Result<bool, ()> {
675 Ok(existing_registrations
676 .iter()
677 .any(|cred| cred.entry.cred_id == *id))
678 })?;
679
680 Ok(TfaEntry::new(reg.description, credential))
681 }
682 }
683
684 /// TFA data for a user.
685 #[derive(Default, Deserialize, Serialize)]
686 #[serde(deny_unknown_fields)]
687 #[serde(rename_all = "kebab-case")]
688 pub struct TfaUserData {
689 /// Totp keys for a user.
690 #[serde(skip_serializing_if = "Vec::is_empty", default)]
691 pub(crate) totp: Vec<TfaEntry<Totp>>,
692
693 /// Registered u2f tokens for a user.
694 #[serde(skip_serializing_if = "Vec::is_empty", default)]
695 pub(crate) u2f: Vec<TfaEntry<u2f::Registration>>,
696
697 /// Registered webauthn tokens for a user.
698 #[serde(skip_serializing_if = "Vec::is_empty", default)]
699 pub(crate) webauthn: Vec<TfaEntry<WebauthnCredential>>,
700
701 /// Recovery keys. (Unordered OTP values).
702 #[serde(skip_serializing_if = "Recovery::option_is_empty", default)]
703 pub(crate) recovery: Option<Recovery>,
704 }
705
706 impl TfaUserData {
707 /// Shortcut to get the recovery entry only if it is not empty!
708 pub fn recovery(&self) -> Option<&Recovery> {
709 if Recovery::option_is_empty(&self.recovery) {
710 None
711 } else {
712 self.recovery.as_ref()
713 }
714 }
715
716 /// `true` if no second factors exist
717 pub fn is_empty(&self) -> bool {
718 self.totp.is_empty()
719 && self.u2f.is_empty()
720 && self.webauthn.is_empty()
721 && self.recovery().is_none()
722 }
723
724 /// Find an entry by id, except for the "recovery" entry which we're currently treating
725 /// specially.
726 pub fn find_entry_mut<'a>(&'a mut self, id: &str) -> Option<&'a mut TfaInfo> {
727 for entry in &mut self.totp {
728 if entry.info.id == id {
729 return Some(&mut entry.info);
730 }
731 }
732
733 for entry in &mut self.webauthn {
734 if entry.info.id == id {
735 return Some(&mut entry.info);
736 }
737 }
738
739 for entry in &mut self.u2f {
740 if entry.info.id == id {
741 return Some(&mut entry.info);
742 }
743 }
744
745 None
746 }
747
748 /// Create a u2f registration challenge.
749 ///
750 /// The description is required at this point already mostly to better be able to identify such
751 /// challenges in the tfa config file if necessary. The user otherwise has no access to this
752 /// information at this point, as the challenge is identified by its actual challenge data
753 /// instead.
754 fn u2f_registration_challenge(
755 &mut self,
756 userid: &Userid,
757 u2f: &u2f::U2f,
758 description: String,
759 ) -> Result<String, Error> {
760 let challenge = serde_json::to_string(&u2f.registration_challenge()?)?;
761
762 let mut data = TfaUserChallengeData::open(userid)?;
763 data.inner
764 .u2f_registrations
765 .push(U2fRegistrationChallenge::new(
766 challenge.clone(),
767 description,
768 ));
769 data.save()?;
770
771 Ok(challenge)
772 }
773
774 fn u2f_registration_finish(
775 &mut self,
776 userid: &Userid,
777 u2f: &u2f::U2f,
778 challenge: &str,
779 response: &str,
780 ) -> Result<String, Error> {
781 let mut data = TfaUserChallengeData::open(userid)?;
782 let entry = data.u2f_registration_finish(u2f, challenge, response)?;
783 data.save()?;
784
785 let id = entry.info.id.clone();
786 self.u2f.push(entry);
787 Ok(id)
788 }
789
790 /// Create a webauthn registration challenge.
791 ///
792 /// The description is required at this point already mostly to better be able to identify such
793 /// challenges in the tfa config file if necessary. The user otherwise has no access to this
794 /// information at this point, as the challenge is identified by its actual challenge data
795 /// instead.
796 fn webauthn_registration_challenge(
797 &mut self,
798 mut webauthn: Webauthn<WebauthnConfig>,
799 userid: &Userid,
800 description: String,
801 ) -> Result<String, Error> {
802 let cred_ids: Vec<_> = self
803 .enabled_webauthn_entries()
804 .map(|cred| cred.cred_id.clone())
805 .collect();
806
807 let userid_str = userid.to_string();
808 let (challenge, state) = webauthn.generate_challenge_register_options(
809 userid_str.as_bytes().to_vec(),
810 userid_str.clone(),
811 userid_str.clone(),
812 Some(cred_ids),
813 Some(UserVerificationPolicy::Discouraged),
814 )?;
815
816 let challenge_string = challenge.public_key.challenge.to_string();
817 let challenge = serde_json::to_string(&challenge)?;
818
819 let mut data = TfaUserChallengeData::open(userid)?;
820 data.inner
821 .webauthn_registrations
822 .push(WebauthnRegistrationChallenge::new(
823 state,
824 challenge_string,
825 description,
826 ));
827 data.save()?;
828
829 Ok(challenge)
830 }
831
832 /// Finish a webauthn registration. The challenge should correspond to an output of
833 /// `webauthn_registration_challenge`. The response should come directly from the client.
834 fn webauthn_registration_finish(
835 &mut self,
836 webauthn: Webauthn<WebauthnConfig>,
837 userid: &Userid,
838 challenge: &str,
839 response: webauthn_rs::proto::RegisterPublicKeyCredential,
840 ) -> Result<String, Error> {
841 let mut data = TfaUserChallengeData::open(userid)?;
842 let entry =
843 data.webauthn_registration_finish(webauthn, challenge, response, &self.webauthn)?;
844 data.save()?;
845
846 let id = entry.info.id.clone();
847 self.webauthn.push(entry);
848 Ok(id)
849 }
850
851 /// Generate a generic TFA challenge. See the [`TfaChallenge`] description for details.
852 pub fn challenge(
853 &mut self,
854 userid: &Userid,
855 webauthn: Option<Webauthn<WebauthnConfig>>,
856 u2f: Option<&u2f::U2f>,
857 ) -> Result<Option<TfaChallenge>, Error> {
858 if self.is_empty() {
859 return Ok(None);
860 }
861
862 Ok(Some(TfaChallenge {
863 totp: self.totp.iter().any(|e| e.info.enable),
864 recovery: RecoveryState::from(&self.recovery),
865 webauthn: match webauthn {
866 Some(webauthn) => self.webauthn_challenge(userid, webauthn)?,
867 None => None,
868 },
869 u2f: match u2f {
870 Some(u2f) => self.u2f_challenge(u2f)?,
871 None => None,
872 },
873 }))
874 }
875
876 /// Helper to iterate over enabled totp entries.
877 fn enabled_totp_entries(&self) -> impl Iterator<Item = &Totp> {
878 self.totp
879 .iter()
880 .filter_map(|e| if e.info.enable { Some(&e.entry) } else { None })
881 }
882
883 /// Helper to iterate over enabled u2f entries.
884 fn enabled_u2f_entries(&self) -> impl Iterator<Item = &u2f::Registration> {
885 self.u2f
886 .iter()
887 .filter_map(|e| if e.info.enable { Some(&e.entry) } else { None })
888 }
889
890 /// Helper to iterate over enabled u2f entries.
891 fn enabled_webauthn_entries(&self) -> impl Iterator<Item = &WebauthnCredential> {
892 self.webauthn
893 .iter()
894 .filter_map(|e| if e.info.enable { Some(&e.entry) } else { None })
895 }
896
897 /// Generate an optional u2f challenge.
898 fn u2f_challenge(&self, u2f: &u2f::U2f) -> Result<Option<U2fChallenge>, Error> {
899 if self.u2f.is_empty() {
900 return Ok(None);
901 }
902
903 let keys: Vec<u2f::RegisteredKey> = self
904 .enabled_u2f_entries()
905 .map(|registration| registration.key.clone())
906 .collect();
907
908 if keys.is_empty() {
909 return Ok(None);
910 }
911
912 Ok(Some(U2fChallenge {
913 challenge: u2f.auth_challenge()?,
914 keys,
915 }))
916 }
917
918 /// Generate an optional webauthn challenge.
919 fn webauthn_challenge(
920 &mut self,
921 userid: &Userid,
922 mut webauthn: Webauthn<WebauthnConfig>,
923 ) -> Result<Option<webauthn_rs::proto::RequestChallengeResponse>, Error> {
924 if self.webauthn.is_empty() {
925 return Ok(None);
926 }
927
928 let creds: Vec<_> = self.enabled_webauthn_entries().map(Clone::clone).collect();
929
930 if creds.is_empty() {
931 return Ok(None);
932 }
933
934 let (challenge, state) = webauthn
935 .generate_challenge_authenticate(creds, Some(UserVerificationPolicy::Discouraged))?;
936 let challenge_string = challenge.public_key.challenge.to_string();
937 let mut data = TfaUserChallengeData::open(userid)?;
938 data.inner
939 .webauthn_auths
940 .push(WebauthnAuthChallenge::new(state, challenge_string));
941 data.save()?;
942
943 Ok(Some(challenge))
944 }
945
946 /// Verify a totp challenge. The `value` should be the totp digits as plain text.
947 fn verify_totp(&self, value: &str) -> Result<(), Error> {
948 let now = std::time::SystemTime::now();
949
950 for entry in self.enabled_totp_entries() {
951 if entry.verify(value, now, -1..=1)?.is_some() {
952 return Ok(());
953 }
954 }
955
956 bail!("totp verification failed");
957 }
958
959 /// Verify a u2f response.
960 fn verify_u2f(
961 &self,
962 u2f: u2f::U2f,
963 challenge: &u2f::AuthChallenge,
964 response: Value,
965 ) -> Result<(), Error> {
966 let response: u2f::AuthResponse = serde_json::from_value(response)
967 .map_err(|err| format_err!("invalid u2f response: {}", err))?;
968
969 if let Some(entry) = self
970 .enabled_u2f_entries()
971 .find(|e| e.key.key_handle == response.key_handle())
972 {
973 if u2f
974 .auth_verify_obj(&entry.public_key, &challenge.challenge, response)?
975 .is_some()
976 {
977 return Ok(());
978 }
979 }
980
981 bail!("u2f verification failed");
982 }
983
984 /// Verify a webauthn response.
985 fn verify_webauthn(
986 &mut self,
987 userid: &Userid,
988 mut webauthn: Webauthn<WebauthnConfig>,
989 mut response: Value,
990 ) -> Result<(), Error> {
991 let expire_before = proxmox_time::epoch_i64() - CHALLENGE_TIMEOUT;
992
993 let challenge = match response
994 .as_object_mut()
995 .ok_or_else(|| format_err!("invalid response, must be a json object"))?
996 .remove("challenge")
997 .ok_or_else(|| format_err!("missing challenge data in response"))?
998 {
999 Value::String(s) => s,
1000 _ => bail!("invalid challenge data in response"),
1001 };
1002
1003 let response: webauthn_rs::proto::PublicKeyCredential = serde_json::from_value(response)
1004 .map_err(|err| format_err!("invalid webauthn response: {}", err))?;
1005
1006 let mut data = match TfaUserChallengeData::open_no_create(userid)? {
1007 Some(data) => data,
1008 None => bail!("no such challenge"),
1009 };
1010
1011 let index = data
1012 .inner
1013 .webauthn_auths
1014 .iter()
1015 .position(|r| r.challenge == challenge)
1016 .ok_or_else(|| format_err!("no such challenge"))?;
1017
1018 let challenge = data.inner.webauthn_auths.remove(index);
1019 if challenge.is_expired(expire_before) {
1020 bail!("no such challenge");
1021 }
1022
1023 // we don't allow re-trying the challenge, so make the removal persistent now:
1024 data.save()
1025 .map_err(|err| format_err!("failed to save challenge file: {}", err))?;
1026
1027 match webauthn.authenticate_credential(response, challenge.state)? {
1028 Some((_cred, _counter)) => Ok(()),
1029 None => bail!("webauthn authentication failed"),
1030 }
1031 }
1032
1033 /// Verify a recovery key.
1034 ///
1035 /// NOTE: If successful, the key will automatically be removed from the list of available
1036 /// recovery keys, so the configuration needs to be saved afterwards!
1037 fn verify_recovery(&mut self, value: &str) -> Result<(), Error> {
1038 if let Some(r) = &mut self.recovery {
1039 if r.verify(value)? {
1040 return Ok(());
1041 }
1042 }
1043 bail!("recovery verification failed");
1044 }
1045
1046 /// Add a new set of recovery keys. There can only be 1 set of keys at a time.
1047 fn add_recovery(&mut self) -> Result<Vec<String>, Error> {
1048 if self.recovery.is_some() {
1049 bail!("user already has recovery keys");
1050 }
1051
1052 let (recovery, original) = Recovery::generate()?;
1053
1054 self.recovery = Some(recovery);
1055
1056 Ok(original)
1057 }
1058 }
1059
1060 /// Recovery entries. We use HMAC-SHA256 with a random secret as a salted hash replacement.
1061 #[derive(Deserialize, Serialize)]
1062 pub struct Recovery {
1063 /// "Salt" used for the key HMAC.
1064 secret: String,
1065
1066 /// Recovery key entries are HMACs of the original data. When used up they will become `None`
1067 /// since the user is presented an enumerated list of codes, so we know the indices of used and
1068 /// unused codes.
1069 entries: Vec<Option<String>>,
1070
1071 /// Creation timestamp as a unix epoch.
1072 pub created: i64,
1073 }
1074
1075 impl Recovery {
1076 /// Generate recovery keys and return the recovery entry along with the original string
1077 /// entries.
1078 fn generate() -> Result<(Self, Vec<String>), Error> {
1079 let mut secret = [0u8; 8];
1080 proxmox::sys::linux::fill_with_random_data(&mut secret)?;
1081
1082 let mut this = Self {
1083 secret: hex::encode(&secret).to_string(),
1084 entries: Vec::with_capacity(10),
1085 created: proxmox_time::epoch_i64(),
1086 };
1087
1088 let mut original = Vec::new();
1089
1090 let mut key_data = [0u8; 80]; // 10 keys of 12 bytes
1091 proxmox::sys::linux::fill_with_random_data(&mut key_data)?;
1092 for b in key_data.chunks(8) {
1093 // unwrap: encoding hex bytes to fixed sized arrays
1094 let entry = format!(
1095 "{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}",
1096 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
1097 );
1098 this.entries.push(Some(this.hash(entry.as_bytes())?));
1099 original.push(entry);
1100 }
1101
1102 Ok((this, original))
1103 }
1104
1105 /// Perform HMAC-SHA256 on the data and return the result as a hex string.
1106 fn hash(&self, data: &[u8]) -> Result<String, Error> {
1107 let secret = PKey::hmac(self.secret.as_bytes())
1108 .map_err(|err| format_err!("error instantiating hmac key: {}", err))?;
1109
1110 let mut signer = Signer::new(MessageDigest::sha256(), &secret)
1111 .map_err(|err| format_err!("error instantiating hmac signer: {}", err))?;
1112
1113 let hmac = signer
1114 .sign_oneshot_to_vec(data)
1115 .map_err(|err| format_err!("error calculating hmac: {}", err))?;
1116
1117 Ok(hex::encode(&hmac))
1118 }
1119
1120 /// Iterator over available keys.
1121 fn available(&self) -> impl Iterator<Item = &str> {
1122 self.entries.iter().filter_map(Option::as_deref)
1123 }
1124
1125 /// Count the available keys.
1126 fn count_available(&self) -> usize {
1127 self.available().count()
1128 }
1129
1130 /// Convenience serde method to check if either the option is `None` or the content `is_empty`.
1131 fn option_is_empty(this: &Option<Self>) -> bool {
1132 this.as_ref()
1133 .map_or(true, |this| this.count_available() == 0)
1134 }
1135
1136 /// Verify a key and remove it. Returns whether the key was valid. Errors on openssl errors.
1137 fn verify(&mut self, key: &str) -> Result<bool, Error> {
1138 let hash = self.hash(key.as_bytes())?;
1139 for entry in &mut self.entries {
1140 if entry.as_ref() == Some(&hash) {
1141 *entry = None;
1142 return Ok(true);
1143 }
1144 }
1145 Ok(false)
1146 }
1147 }
1148
1149 /// Serde helper using our `FilteredVecVisitor` to filter out expired entries directly at load
1150 /// time.
1151 fn filter_expired_challenge<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
1152 where
1153 D: Deserializer<'de>,
1154 T: Deserialize<'de> + IsExpired,
1155 {
1156 let expire_before = proxmox_time::epoch_i64() - CHALLENGE_TIMEOUT;
1157 Ok(
1158 deserializer.deserialize_seq(crate::tools::serde_filter::FilteredVecVisitor::new(
1159 "a challenge entry",
1160 move |reg: &T| !reg.is_expired(expire_before),
1161 ))?,
1162 )
1163 }
1164
1165 /// Get an optional TFA challenge for a user.
1166 pub fn login_challenge(userid: &Userid) -> Result<Option<TfaChallenge>, Error> {
1167 let _lock = write_lock()?;
1168
1169 let mut data = read()?;
1170 Ok(match data.login_challenge(userid)? {
1171 Some(challenge) => {
1172 write(&data)?;
1173 Some(challenge)
1174 }
1175 None => None,
1176 })
1177 }
1178
1179 /// Add a TOTP entry for a user. Returns the ID.
1180 pub fn add_totp(userid: &Userid, description: String, value: Totp) -> Result<String, Error> {
1181 let _lock = write_lock();
1182 let mut data = read()?;
1183 let entry = TfaEntry::new(description, value);
1184 let id = entry.info.id.clone();
1185 data.users
1186 .entry(userid.clone())
1187 .or_default()
1188 .totp
1189 .push(entry);
1190 write(&data)?;
1191 Ok(id)
1192 }
1193
1194 /// Add recovery tokens for the user. Returns the token list.
1195 pub fn add_recovery(userid: &Userid) -> Result<Vec<String>, Error> {
1196 let _lock = write_lock();
1197
1198 let mut data = read()?;
1199 let out = data
1200 .users
1201 .entry(userid.clone())
1202 .or_default()
1203 .add_recovery()?;
1204 write(&data)?;
1205 Ok(out)
1206 }
1207
1208 /// Add a u2f registration challenge for a user.
1209 pub fn add_u2f_registration(userid: &Userid, description: String) -> Result<String, Error> {
1210 let _lock = crate::config::tfa::write_lock();
1211 let mut data = read()?;
1212 let challenge = data.u2f_registration_challenge(userid, description)?;
1213 write(&data)?;
1214 Ok(challenge)
1215 }
1216
1217 /// Finish a u2f registration challenge for a user.
1218 pub fn finish_u2f_registration(
1219 userid: &Userid,
1220 challenge: &str,
1221 response: &str,
1222 ) -> Result<String, Error> {
1223 let _lock = crate::config::tfa::write_lock();
1224 let mut data = read()?;
1225 let id = data.u2f_registration_finish(userid, challenge, response)?;
1226 write(&data)?;
1227 Ok(id)
1228 }
1229
1230 /// Add a webauthn registration challenge for a user.
1231 pub fn add_webauthn_registration(userid: &Userid, description: String) -> Result<String, Error> {
1232 let _lock = crate::config::tfa::write_lock();
1233 let mut data = read()?;
1234 let challenge = data.webauthn_registration_challenge(userid, description)?;
1235 write(&data)?;
1236 Ok(challenge)
1237 }
1238
1239 /// Finish a webauthn registration challenge for a user.
1240 pub fn finish_webauthn_registration(
1241 userid: &Userid,
1242 challenge: &str,
1243 response: &str,
1244 ) -> Result<String, Error> {
1245 let _lock = crate::config::tfa::write_lock();
1246 let mut data = read()?;
1247 let id = data.webauthn_registration_finish(userid, challenge, response)?;
1248 write(&data)?;
1249 Ok(id)
1250 }
1251
1252 /// Verify a TFA challenge.
1253 pub fn verify_challenge(
1254 userid: &Userid,
1255 challenge: &TfaChallenge,
1256 response: TfaResponse,
1257 ) -> Result<(), Error> {
1258 let _lock = crate::config::tfa::write_lock();
1259 let mut data = read()?;
1260 data.verify(userid, challenge, response)?;
1261 write(&data)?;
1262 Ok(())
1263 }
1264
1265 /// Used to inform the user about the recovery code status.
1266 ///
1267 /// This contains the available key indices.
1268 #[derive(Clone, Default, Eq, PartialEq, Deserialize, Serialize)]
1269 pub struct RecoveryState(Vec<usize>);
1270
1271 impl RecoveryState {
1272 fn is_unavailable(&self) -> bool {
1273 self.0.is_empty()
1274 }
1275 }
1276
1277 impl From<&Option<Recovery>> for RecoveryState {
1278 fn from(r: &Option<Recovery>) -> Self {
1279 match r {
1280 Some(r) => Self::from(r),
1281 None => Self::default(),
1282 }
1283 }
1284 }
1285
1286 impl From<&Recovery> for RecoveryState {
1287 fn from(r: &Recovery) -> Self {
1288 Self(
1289 r.entries
1290 .iter()
1291 .enumerate()
1292 .filter_map(|(idx, key)| if key.is_some() { Some(idx) } else { None })
1293 .collect(),
1294 )
1295 }
1296 }
1297
1298 /// When sending a TFA challenge to the user, we include information about what kind of challenge
1299 /// the user may perform. If webauthn credentials are available, a webauthn challenge will be
1300 /// included.
1301 #[derive(Deserialize, Serialize)]
1302 #[serde(rename_all = "kebab-case")]
1303 pub struct TfaChallenge {
1304 /// True if the user has TOTP devices.
1305 totp: bool,
1306
1307 /// Whether there are recovery keys available.
1308 #[serde(skip_serializing_if = "RecoveryState::is_unavailable", default)]
1309 recovery: RecoveryState,
1310
1311 /// If the user has any u2f tokens registered, this will contain the U2F challenge data.
1312 #[serde(skip_serializing_if = "Option::is_none")]
1313 u2f: Option<U2fChallenge>,
1314
1315 /// If the user has any webauthn credentials registered, this will contain the corresponding
1316 /// challenge data.
1317 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1318 webauthn: Option<webauthn_rs::proto::RequestChallengeResponse>,
1319 }
1320
1321 /// Data used for u2f challenges.
1322 #[derive(Deserialize, Serialize)]
1323 pub struct U2fChallenge {
1324 /// AppID and challenge data.
1325 challenge: u2f::AuthChallenge,
1326
1327 /// Available tokens/keys.
1328 keys: Vec<u2f::RegisteredKey>,
1329 }
1330
1331 /// A user's response to a TFA challenge.
1332 pub enum TfaResponse {
1333 Totp(String),
1334 U2f(Value),
1335 Webauthn(Value),
1336 Recovery(String),
1337 }
1338
1339 impl std::str::FromStr for TfaResponse {
1340 type Err = Error;
1341
1342 fn from_str(s: &str) -> Result<Self, Error> {
1343 Ok(if let Some(totp) = s.strip_prefix("totp:") {
1344 TfaResponse::Totp(totp.to_string())
1345 } else if let Some(u2f) = s.strip_prefix("u2f:") {
1346 TfaResponse::U2f(serde_json::from_str(u2f)?)
1347 } else if let Some(webauthn) = s.strip_prefix("webauthn:") {
1348 TfaResponse::Webauthn(serde_json::from_str(webauthn)?)
1349 } else if let Some(recovery) = s.strip_prefix("recovery:") {
1350 TfaResponse::Recovery(recovery.to_string())
1351 } else {
1352 bail!("invalid tfa response");
1353 })
1354 }
1355 }
1356
1357 const fn default_tfa_enable() -> bool {
1358 true
1359 }
1360
1361 const fn is_default_tfa_enable(v: &bool) -> bool {
1362 *v
1363 }