1 //! TFA configuration and user data.
3 //! This is the same as used in PBS but without the `#[api]` type.
5 //! We may want to move this into a shared crate making the `#[api]` macro feature-gated!
7 use std
::collections
::HashMap
;
9 use anyhow
::{bail, format_err, Error}
;
10 use serde
::{Deserialize, Serialize}
;
11 use serde_json
::Value
;
14 use webauthn_rs
::{proto::UserVerificationPolicy, Webauthn}
;
16 use crate::totp
::Totp
;
17 use proxmox_uuid
::Uuid
;
19 #[cfg(feature = "api-types")]
20 use proxmox_schema
::api
;
30 pub use recovery
::RecoveryState
;
31 pub use u2f
::U2fConfig
;
32 use webauthn
::WebauthnConfigInstance
;
33 pub use webauthn
::{WebauthnConfig, WebauthnCredential}
;
35 #[cfg(feature = "api-types")]
36 pub use webauthn
::WebauthnConfigUpdater
;
38 use recovery
::Recovery
;
39 use u2f
::{U2fChallenge, U2fChallengeEntry, U2fRegistrationChallenge}
;
40 use webauthn
::{WebauthnAuthChallenge, WebauthnRegistrationChallenge}
;
43 fn is_expired(&self, at_epoch
: i64) -> bool
;
46 pub trait OpenUserChallengeData
: Clone
{
47 type Data
: UserChallengeAccess
;
49 fn open(&self, userid
: &str) -> Result
<Self::Data
, Error
>;
51 fn open_no_create(&self, userid
: &str) -> Result
<Option
<Self::Data
>, Error
>;
53 /// Should return `true` if something was removed, `false` if no data existed for the user.
54 fn remove(&self, userid
: &str) -> Result
<bool
, Error
>;
57 pub trait UserChallengeAccess
: Sized
{
58 //fn open(userid: &str) -> Result<Self, Error>;
59 //fn open_no_create(userid: &str) -> Result<Option<Self>, Error>;
60 fn get_mut(&mut self) -> &mut TfaUserChallenges
;
61 fn save(self) -> Result
<(), Error
>;
64 const CHALLENGE_TIMEOUT_SECS
: i64 = 2 * 60;
66 /// TFA Configuration for this instance.
67 #[derive(Clone, Default, Deserialize, Serialize)]
68 pub struct TfaConfig
{
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub u2f
: Option
<U2fConfig
>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub webauthn
: Option
<WebauthnConfig
>,
75 #[serde(skip_serializing_if = "TfaUsers::is_empty", default)]
79 /// Helper to get a u2f instance from a u2f config, or `None` if there isn't one configured.
80 fn get_u2f(u2f
: &Option
<U2fConfig
>) -> Option
<u2f
::U2f
> {
81 u2f
.as_ref().map(|cfg
| {
84 cfg
.origin
.clone().unwrap_or_else(|| cfg
.appid
.clone()),
89 /// Helper to get a u2f instance from a u2f config.
91 /// This is outside of `TfaConfig` to not borrow its `&self`.
92 fn check_u2f(u2f
: &Option
<U2fConfig
>) -> Result
<u2f
::U2f
, Error
> {
93 get_u2f(u2f
).ok_or_else(|| format_err
!("no u2f configuration available"))
96 /// Helper to get a `Webauthn` instance from a `WebauthnConfig`, or `None` if there isn't one
98 fn get_webauthn
<'a
, 'config
: 'a
, 'origin
: 'a
>(
99 waconfig
: &'config Option
<WebauthnConfig
>,
100 origin
: Option
<&'origin Url
>,
101 ) -> Option
<Result
<Webauthn
<WebauthnConfigInstance
<'a
>>, Error
>> {
102 Some(waconfig
.as_ref()?
.instantiate(origin
).map(Webauthn
::new
))
105 /// Helper to get a u2f instance from a u2f config.
107 /// This is outside of `TfaConfig` to not borrow its `&self`.
108 fn check_webauthn
<'a
, 'config
: 'a
, 'origin
: 'a
>(
109 waconfig
: &'config Option
<WebauthnConfig
>,
110 origin
: Option
<&'origin Url
>,
111 ) -> Result
<Webauthn
<WebauthnConfigInstance
<'a
>>, Error
> {
112 get_webauthn(waconfig
, origin
)
113 .ok_or_else(|| format_err
!("no webauthn configuration available"))?
117 // Get a u2f registration challenge.
118 pub fn u2f_registration_challenge
<A
: OpenUserChallengeData
>(
123 ) -> Result
<String
, Error
> {
124 let u2f
= check_u2f(&self.u2f
)?
;
127 .entry(userid
.to_owned())
129 .u2f_registration_challenge(access
, userid
, &u2f
, description
)
132 /// Finish a u2f registration challenge.
133 pub fn u2f_registration_finish
<A
: OpenUserChallengeData
>(
139 ) -> Result
<String
, Error
> {
140 let u2f
= check_u2f(&self.u2f
)?
;
142 match self.users
.get_mut(userid
) {
143 Some(user
) => user
.u2f_registration_finish(access
, userid
, &u2f
, challenge
, response
),
144 None
=> bail
!("no such challenge"),
148 /// Get a webauthn registration challenge.
149 pub fn webauthn_registration_challenge
<A
: OpenUserChallengeData
>(
154 origin
: Option
<&Url
>,
155 ) -> Result
<String
, Error
> {
156 let webauthn
= check_webauthn(&self.webauthn
, origin
)?
;
159 .entry(user
.to_owned())
161 .webauthn_registration_challenge(access
, webauthn
, user
, description
)
164 /// Finish a webauthn registration challenge.
165 pub fn webauthn_registration_finish
<A
: OpenUserChallengeData
>(
171 origin
: Option
<&Url
>,
172 ) -> Result
<String
, Error
> {
173 let webauthn
= check_webauthn(&self.webauthn
, origin
)?
;
175 let response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
=
176 serde_json
::from_str(response
)
177 .map_err(|err
| format_err
!("error parsing challenge response: {}", err
))?
;
179 match self.users
.get_mut(userid
) {
181 user
.webauthn_registration_finish(access
, webauthn
, userid
, challenge
, response
)
183 None
=> bail
!("no such challenge"),
187 /// Add a TOTP entry for a user.
189 /// Unlike U2F/WA, this does not require a challenge/response. The user can choose their secret
191 pub fn add_totp(&mut self, userid
: &str, description
: String
, value
: Totp
) -> String
{
193 .entry(userid
.to_owned())
195 .add_totp(description
, value
)
198 /// Add a Yubico key to a user.
200 /// Unlike U2F/WA, this does not require a challenge/response. The user can choose their secret
202 pub fn add_yubico(&mut self, userid
: &str, description
: String
, key
: String
) -> String
{
204 .entry(userid
.to_owned())
206 .add_yubico(description
, key
)
209 /// Add a new set of recovery keys. There can only be 1 set of keys at a time.
210 pub fn add_recovery(&mut self, userid
: &str) -> Result
<Vec
<String
>, Error
> {
212 .entry(userid
.to_owned())
217 /// Get a two factor authentication challenge for a user, if the user has TFA set up.
218 pub fn authentication_challenge
<A
: OpenUserChallengeData
>(
222 origin
: Option
<&Url
>,
223 ) -> Result
<Option
<TfaChallenge
>, Error
> {
224 match self.users
.get_mut(userid
) {
225 Some(udata
) => udata
.challenge(
228 get_webauthn(&self.webauthn
, origin
),
229 get_u2f(&self.u2f
).as_ref(),
235 /// Verify a TFA challenge.
236 pub fn verify
<A
: OpenUserChallengeData
>(
240 challenge
: &TfaChallenge
,
241 response
: TfaResponse
,
242 origin
: Option
<&Url
>,
243 ) -> Result
<NeedsSaving
, Error
> {
244 match self.users
.get_mut(userid
) {
245 Some(user
) => match response
{
246 TfaResponse
::Totp(value
) => user
.verify_totp(&value
),
247 TfaResponse
::U2f(value
) => match &challenge
.u2f
{
249 let u2f
= check_u2f(&self.u2f
)?
;
250 user
.verify_u2f(access
, userid
, u2f
, &challenge
.challenge
, value
)
252 None
=> bail
!("no u2f factor available for user '{}'", userid
),
254 TfaResponse
::Webauthn(value
) => {
255 let webauthn
= check_webauthn(&self.webauthn
, origin
)?
;
256 user
.verify_webauthn(access
, userid
, webauthn
, value
)
258 TfaResponse
::Recovery(value
) => {
259 user
.verify_recovery(&value
)?
;
260 return Ok(NeedsSaving
::Yes
);
263 None
=> bail
!("no 2nd factor available for user '{}'", userid
),
269 pub fn remove_user
<A
: OpenUserChallengeData
>(
273 ) -> Result
<NeedsSaving
, Error
> {
274 let mut save
= access
.remove(userid
)?
;
275 if self.users
.remove(userid
).is_some() {
282 #[must_use = "must save the config in order to ensure one-time use of recovery keys"]
283 #[derive(Clone, Copy)]
284 pub enum NeedsSaving
{
290 /// Convenience method so we don't need to import the type name.
291 pub fn needs_saving(self) -> bool
{
292 matches
!(self, NeedsSaving
::Yes
)
296 impl From
<bool
> for NeedsSaving
{
297 fn from(v
: bool
) -> Self {
306 /// Mapping of userid to TFA entry.
307 pub type TfaUsers
= HashMap
<String
, TfaUserData
>;
309 /// TFA data for a user.
310 #[derive(Clone, Default, Deserialize, Serialize)]
311 #[serde(deny_unknown_fields)]
312 #[serde(rename_all = "kebab-case")]
313 pub struct TfaUserData
{
314 /// Totp keys for a user.
315 #[serde(skip_serializing_if = "Vec::is_empty", default)]
316 pub totp
: Vec
<TfaEntry
<Totp
>>,
318 /// Registered u2f tokens for a user.
319 #[serde(skip_serializing_if = "Vec::is_empty", default)]
320 pub u2f
: Vec
<TfaEntry
<u2f
::Registration
>>,
322 /// Registered webauthn tokens for a user.
323 #[serde(skip_serializing_if = "Vec::is_empty", default)]
324 pub webauthn
: Vec
<TfaEntry
<WebauthnCredential
>>,
326 /// Recovery keys. (Unordered OTP values).
327 #[serde(skip_serializing_if = "Recovery::option_is_empty", default)]
328 pub recovery
: Option
<Recovery
>,
330 /// Yubico keys for a user. NOTE: This is not directly supported currently, we just need this
331 /// available for PVE, where the yubico API server configuration is part if the realm.
332 #[serde(skip_serializing_if = "Vec::is_empty", default)]
333 pub yubico
: Vec
<TfaEntry
<String
>>,
337 /// Shortcut to get the recovery entry only if it is not empty!
338 pub fn recovery(&self) -> Option
<&Recovery
> {
339 if Recovery
::option_is_empty(&self.recovery
) {
342 self.recovery
.as_ref()
346 /// `true` if no second factors exist
347 pub fn is_empty(&self) -> bool
{
349 && self.u2f
.is_empty()
350 && self.webauthn
.is_empty()
351 && self.yubico
.is_empty()
352 && self.recovery().is_none()
355 /// Find an entry by id, except for the "recovery" entry which we're currently treating
357 pub fn find_entry_mut
<'a
>(&'a
mut self, id
: &str) -> Option
<&'a
mut TfaInfo
> {
358 for entry
in &mut self.totp
{
359 if entry
.info
.id
== id
{
360 return Some(&mut entry
.info
);
364 for entry
in &mut self.webauthn
{
365 if entry
.info
.id
== id
{
366 return Some(&mut entry
.info
);
370 for entry
in &mut self.u2f
{
371 if entry
.info
.id
== id
{
372 return Some(&mut entry
.info
);
376 for entry
in &mut self.yubico
{
377 if entry
.info
.id
== id
{
378 return Some(&mut entry
.info
);
385 /// Create a u2f registration challenge.
387 /// The description is required at this point already mostly to better be able to identify such
388 /// challenges in the tfa config file if necessary. The user otherwise has no access to this
389 /// information at this point, as the challenge is identified by its actual challenge data
391 fn u2f_registration_challenge
<A
: OpenUserChallengeData
>(
397 ) -> Result
<String
, Error
> {
398 let challenge
= serde_json
::to_string(&u2f
.registration_challenge()?
)?
;
400 let mut data
= access
.open(userid
)?
;
403 .push(U2fRegistrationChallenge
::new(
412 fn u2f_registration_finish
<A
: OpenUserChallengeData
>(
419 ) -> Result
<String
, Error
> {
420 let mut data
= access
.open(userid
)?
;
423 .u2f_registration_finish(u2f
, challenge
, response
)?
;
426 let id
= entry
.info
.id
.clone();
427 self.u2f
.push(entry
);
431 /// Create a webauthn registration challenge.
433 /// The description is required at this point already mostly to better be able to identify such
434 /// challenges in the tfa config file if necessary. The user otherwise has no access to this
435 /// information at this point, as the challenge is identified by its actual challenge data
437 fn webauthn_registration_challenge
<A
: OpenUserChallengeData
>(
440 webauthn
: Webauthn
<WebauthnConfigInstance
>,
443 ) -> Result
<String
, Error
> {
444 let cred_ids
: Vec
<_
> = self
445 .enabled_webauthn_entries()
446 .map(|cred
| cred
.cred_id
.clone())
449 let (challenge
, state
) = webauthn
.generate_challenge_register_options(
450 userid
.as_bytes().to_vec(),
454 Some(UserVerificationPolicy
::Discouraged
),
458 let challenge_string
= challenge
.public_key
.challenge
.to_string();
459 let challenge
= serde_json
::to_string(&challenge
)?
;
461 let mut data
= access
.open(userid
)?
;
463 .webauthn_registrations
464 .push(WebauthnRegistrationChallenge
::new(
474 /// Finish a webauthn registration. The challenge should correspond to an output of
475 /// `webauthn_registration_challenge`. The response should come directly from the client.
476 fn webauthn_registration_finish
<A
: OpenUserChallengeData
>(
479 webauthn
: Webauthn
<WebauthnConfigInstance
>,
482 response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
,
483 ) -> Result
<String
, Error
> {
484 let mut data
= access
.open(userid
)?
;
485 let entry
= data
.get_mut().webauthn_registration_finish(
493 let id
= entry
.info
.id
.clone();
494 self.webauthn
.push(entry
);
498 fn add_totp(&mut self, description
: String
, totp
: Totp
) -> String
{
499 let entry
= TfaEntry
::new(description
, totp
);
500 let id
= entry
.info
.id
.clone();
501 self.totp
.push(entry
);
505 fn add_yubico(&mut self, description
: String
, key
: String
) -> String
{
506 let entry
= TfaEntry
::new(description
, key
);
507 let id
= entry
.info
.id
.clone();
508 self.yubico
.push(entry
);
512 /// Add a new set of recovery keys. There can only be 1 set of keys at a time.
513 fn add_recovery(&mut self) -> Result
<Vec
<String
>, Error
> {
514 if self.recovery
.is_some() {
515 bail
!("user already has recovery keys");
518 let (recovery
, original
) = Recovery
::generate()?
;
520 self.recovery
= Some(recovery
);
525 /// Helper to iterate over enabled totp entries.
526 fn enabled_totp_entries(&self) -> impl Iterator
<Item
= &Totp
> {
529 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
532 /// Helper to iterate over enabled u2f entries.
533 fn enabled_u2f_entries(&self) -> impl Iterator
<Item
= &u2f
::Registration
> {
536 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
539 /// Helper to iterate over enabled u2f entries.
540 fn enabled_webauthn_entries(&self) -> impl Iterator
<Item
= &WebauthnCredential
> {
543 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
546 /// Helper to iterate over enabled yubico entries.
547 pub fn enabled_yubico_entries(&self) -> impl Iterator
<Item
= &str> {
548 self.yubico
.iter().filter_map(|e
| {
550 Some(e
.entry
.as_str())
557 /// Verify a totp challenge. The `value` should be the totp digits as plain text.
558 fn verify_totp(&self, value
: &str) -> Result
<(), Error
> {
559 let now
= std
::time
::SystemTime
::now();
561 for entry
in self.enabled_totp_entries() {
562 if entry
.verify(value
, now
, -1..=1)?
.is_some() {
567 bail
!("totp verification failed");
570 /// Generate a generic TFA challenge. See the [`TfaChallenge`] description for details.
571 fn challenge
<A
: OpenUserChallengeData
>(
575 webauthn
: Option
<Result
<Webauthn
<WebauthnConfigInstance
>, Error
>>,
576 u2f
: Option
<&u2f
::U2f
>,
577 ) -> Result
<Option
<TfaChallenge
>, Error
> {
582 Ok(Some(TfaChallenge
{
583 totp
: self.totp
.iter().any(|e
| e
.info
.enable
),
584 recovery
: RecoveryState
::from(&self.recovery
),
585 webauthn
: match webauthn
{
586 Some(webauthn
) => self.webauthn_challenge(access
.clone(), userid
, webauthn?
)?
,
590 Some(u2f
) => self.u2f_challenge(access
, userid
, u2f
)?
,
593 yubico
: self.yubico
.iter().any(|e
| e
.info
.enable
),
597 /// Get the recovery state.
598 pub fn recovery_state(&self) -> RecoveryState
{
599 RecoveryState
::from(&self.recovery
)
602 /// Generate an optional webauthn challenge.
603 fn webauthn_challenge
<A
: OpenUserChallengeData
>(
607 webauthn
: Webauthn
<WebauthnConfigInstance
>,
608 ) -> Result
<Option
<webauthn_rs
::proto
::RequestChallengeResponse
>, Error
> {
609 if self.webauthn
.is_empty() {
613 let creds
: Vec
<_
> = self
614 .enabled_webauthn_entries()
615 .map(|cred
| cred
.clone().into())
618 if creds
.is_empty() {
622 let (challenge
, state
) = webauthn
.generate_challenge_authenticate(creds
)?
;
624 let challenge_string
= challenge
.public_key
.challenge
.to_string();
625 let mut data
= access
.open(userid
)?
;
628 .push(WebauthnAuthChallenge
::new(state
, challenge_string
));
634 /// Generate an optional u2f challenge.
635 fn u2f_challenge
<A
: OpenUserChallengeData
>(
640 ) -> Result
<Option
<U2fChallenge
>, Error
> {
641 if self.u2f
.is_empty() {
645 let keys
: Vec
<crate::u2f
::RegisteredKey
> = self
646 .enabled_u2f_entries()
647 .map(|registration
| registration
.key
.clone())
654 let challenge
= U2fChallenge
{
655 challenge
: u2f
.auth_challenge()?
,
659 let mut data
= access
.open(userid
)?
;
662 .push(U2fChallengeEntry
::new(&challenge
));
668 /// Verify a u2f response.
669 fn verify_u2f
<A
: OpenUserChallengeData
>(
674 challenge
: &crate::u2f
::AuthChallenge
,
676 ) -> Result
<(), Error
> {
677 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT_SECS
;
679 let response
: crate::u2f
::AuthResponse
= serde_json
::from_value(response
)
680 .map_err(|err
| format_err
!("invalid u2f response: {}", err
))?
;
682 if let Some(entry
) = self
683 .enabled_u2f_entries()
684 .find(|e
| e
.key
.key_handle
== response
.key_handle())
687 .auth_verify_obj(&entry
.public_key
, &challenge
.challenge
, response
)?
690 let mut data
= match access
.open_no_create(userid
)?
{
692 None
=> bail
!("no such challenge"),
698 .position(|r
| r
== challenge
)
699 .ok_or_else(|| format_err
!("no such challenge"))?
;
700 let entry
= data
.get_mut().u2f_auths
.remove(index
);
701 if entry
.is_expired(expire_before
) {
702 bail
!("no such challenge");
705 .map_err(|err
| format_err
!("failed to save challenge file: {}", err
))?
;
711 bail
!("u2f verification failed");
714 /// Verify a webauthn response.
715 fn verify_webauthn
<A
: OpenUserChallengeData
>(
719 webauthn
: Webauthn
<WebauthnConfigInstance
>,
721 ) -> Result
<(), Error
> {
722 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT_SECS
;
724 let challenge
= match response
726 .ok_or_else(|| format_err
!("invalid response, must be a json object"))?
728 .ok_or_else(|| format_err
!("missing challenge data in response"))?
730 Value
::String(s
) => s
,
731 _
=> bail
!("invalid challenge data in response"),
734 let response
: webauthn_rs
::proto
::PublicKeyCredential
= serde_json
::from_value(response
)
735 .map_err(|err
| format_err
!("invalid webauthn response: {}", err
))?
;
737 let mut data
= match access
.open_no_create(userid
)?
{
739 None
=> bail
!("no such challenge"),
746 .position(|r
| r
.challenge
== challenge
)
747 .ok_or_else(|| format_err
!("no such challenge"))?
;
749 let challenge
= data
.get_mut().webauthn_auths
.remove(index
);
750 if challenge
.is_expired(expire_before
) {
751 bail
!("no such challenge");
754 // we don't allow re-trying the challenge, so make the removal persistent now:
756 .map_err(|err
| format_err
!("failed to save challenge file: {}", err
))?
;
758 webauthn
.authenticate_credential(&response
, &challenge
.state
)?
;
763 /// Verify a recovery key.
765 /// NOTE: If successful, the key will automatically be removed from the list of available
766 /// recovery keys, so the configuration needs to be saved afterwards!
767 fn verify_recovery(&mut self, value
: &str) -> Result
<(), Error
> {
768 if let Some(r
) = &mut self.recovery
{
769 if r
.verify(value
)?
{
773 bail
!("recovery verification failed");
777 /// A TFA entry for a user.
779 /// This simply connects a raw registration to a non optional descriptive text chosen by the user.
780 #[derive(Clone, Deserialize, Serialize)]
781 #[serde(deny_unknown_fields)]
782 pub struct TfaEntry
<T
> {
786 /// The actual entry.
790 impl<T
> TfaEntry
<T
> {
791 /// Create an entry with a description. The id will be autogenerated.
792 fn new(description
: String
, entry
: T
) -> Self {
795 id
: Uuid
::generate().to_string(),
798 created
: proxmox_time
::epoch_i64(),
804 /// Create a raw entry from a `TfaInfo` and the corresponding entry data.
805 pub fn from_parts(info
: TfaInfo
, entry
: T
) -> Self {
810 #[cfg_attr(feature = "api-types", api)]
811 /// Over the API we only provide this part when querying a user's second factor list.
812 #[derive(Clone, Deserialize, Serialize)]
813 #[serde(deny_unknown_fields)]
815 /// The id used to reference this entry.
818 /// User chosen description for this entry.
819 #[serde(skip_serializing_if = "String::is_empty")]
820 pub description
: String
,
822 /// Creation time of this entry as unix epoch.
825 /// Whether this TFA entry is currently enabled.
826 #[serde(skip_serializing_if = "is_default_tfa_enable")]
827 #[serde(default = "default_tfa_enable")]
832 /// For recovery keys we have a fixed entry.
833 pub fn recovery(created
: i64) -> Self {
835 id
: "recovery".to_string(),
836 description
: String
::new(),
843 const fn default_tfa_enable() -> bool
{
847 const fn is_default_tfa_enable(v
: &bool
) -> bool
{
851 /// When sending a TFA challenge to the user, we include information about what kind of challenge
852 /// the user may perform. If webauthn credentials are available, a webauthn challenge will be
854 #[derive(Deserialize, Serialize)]
855 #[serde(rename_all = "kebab-case")]
856 pub struct TfaChallenge
{
857 /// True if the user has TOTP devices.
858 #[serde(skip_serializing_if = "bool_is_false", default)]
861 /// Whether there are recovery keys available.
862 #[serde(skip_serializing_if = "RecoveryState::is_unavailable", default)]
863 pub recovery
: RecoveryState
,
865 /// If the user has any u2f tokens registered, this will contain the U2F challenge data.
866 #[serde(skip_serializing_if = "Option::is_none")]
867 pub u2f
: Option
<U2fChallenge
>,
869 /// If the user has any webauthn credentials registered, this will contain the corresponding
871 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
872 pub webauthn
: Option
<webauthn_rs
::proto
::RequestChallengeResponse
>,
874 /// True if the user has yubico keys configured.
875 #[serde(skip_serializing_if = "bool_is_false", default)]
879 fn bool_is_false(v
: &bool
) -> bool
{
883 /// A user's response to a TFA challenge.
884 pub enum TfaResponse
{
891 /// This is part of the REST API:
892 impl std
::str::FromStr
for TfaResponse
{
895 fn from_str(s
: &str) -> Result
<Self, Error
> {
896 Ok(if let Some(totp
) = s
.strip_prefix("totp:") {
897 TfaResponse
::Totp(totp
.to_string())
898 } else if let Some(u2f
) = s
.strip_prefix("u2f:") {
899 TfaResponse
::U2f(serde_json
::from_str(u2f
)?
)
900 } else if let Some(webauthn
) = s
.strip_prefix("webauthn:") {
901 TfaResponse
::Webauthn(serde_json
::from_str(webauthn
)?
)
902 } else if let Some(recovery
) = s
.strip_prefix("recovery:") {
903 TfaResponse
::Recovery(recovery
.to_string())
905 bail
!("invalid tfa response");
910 /// Active TFA challenges per user, stored in a restricted temporary file on the machine handling
911 /// the current user's authentication.
912 #[derive(Default, Deserialize, Serialize)]
913 pub struct TfaUserChallenges
{
914 /// Active u2f registration challenges for a user.
916 /// Expired values are automatically filtered out while parsing the tfa configuration file.
917 #[serde(skip_serializing_if = "Vec::is_empty", default)]
918 #[serde(deserialize_with = "filter_expired_challenge")]
919 u2f_registrations
: Vec
<U2fRegistrationChallenge
>,
921 /// Active u2f authentication challenges for a user.
923 /// Expired values are automatically filtered out while parsing the tfa configuration file.
924 #[serde(skip_serializing_if = "Vec::is_empty", default)]
925 #[serde(deserialize_with = "filter_expired_challenge")]
926 u2f_auths
: Vec
<U2fChallengeEntry
>,
928 /// Active webauthn registration challenges for a user.
930 /// Expired values are automatically filtered out while parsing the tfa configuration file.
931 #[serde(skip_serializing_if = "Vec::is_empty", default)]
932 #[serde(deserialize_with = "filter_expired_challenge")]
933 webauthn_registrations
: Vec
<WebauthnRegistrationChallenge
>,
935 /// Active webauthn authentication challenges for a user.
937 /// Expired values are automatically filtered out while parsing the tfa configuration file.
938 #[serde(skip_serializing_if = "Vec::is_empty", default)]
939 #[serde(deserialize_with = "filter_expired_challenge")]
940 webauthn_auths
: Vec
<WebauthnAuthChallenge
>,
943 /// Serde helper using our `FilteredVecVisitor` to filter out expired entries directly at load
945 fn filter_expired_challenge
<'de
, D
, T
>(deserializer
: D
) -> Result
<Vec
<T
>, D
::Error
>
947 D
: serde
::Deserializer
<'de
>,
948 T
: Deserialize
<'de
> + IsExpired
,
950 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT_SECS
;
951 deserializer
.deserialize_seq(serde_tools
::fold(
953 |cap
| cap
.map(Vec
::with_capacity
).unwrap_or_else(Vec
::new
),
955 if !reg
.is_expired(expire_before
) {
962 impl TfaUserChallenges
{
963 /// Finish a u2f registration. The challenge should correspond to an output of
964 /// `u2f_registration_challenge` (which is a stringified `RegistrationChallenge`). The response
965 /// should come directly from the client.
966 fn u2f_registration_finish(
971 ) -> Result
<TfaEntry
<u2f
::Registration
>, Error
> {
972 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT_SECS
;
977 .position(|r
| r
.challenge
== challenge
)
978 .ok_or_else(|| format_err
!("no such challenge"))?
;
980 let reg
= &self.u2f_registrations
[index
];
981 if reg
.is_expired(expire_before
) {
982 bail
!("no such challenge");
985 // the verify call only takes the actual challenge string, so we have to extract it
986 // (u2f::RegistrationChallenge did not always implement Deserialize...)
987 let chobj
: Value
= serde_json
::from_str(challenge
)
988 .map_err(|err
| format_err
!("error parsing original registration challenge: {}", err
))?
;
989 let challenge
= chobj
["challenge"]
991 .ok_or_else(|| format_err
!("invalid registration challenge"))?
;
993 let (mut reg
, description
) = match u2f
.registration_verify(challenge
, response
)?
{
994 None
=> bail
!("verification failed"),
996 let entry
= self.u2f_registrations
.remove(index
);
997 (reg
, entry
.description
)
1001 // we do not care about the attestation certificates, so don't store them
1002 reg
.certificate
.clear();
1004 Ok(TfaEntry
::new(description
, reg
))
1007 /// Finish a webauthn registration. The challenge should correspond to an output of
1008 /// `webauthn_registration_challenge`. The response should come directly from the client.
1009 fn webauthn_registration_finish(
1011 webauthn
: Webauthn
<WebauthnConfigInstance
>,
1013 response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
,
1014 existing_registrations
: &[TfaEntry
<WebauthnCredential
>],
1015 ) -> Result
<TfaEntry
<WebauthnCredential
>, Error
> {
1016 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT_SECS
;
1019 .webauthn_registrations
1021 .position(|r
| r
.challenge
== challenge
)
1022 .ok_or_else(|| format_err
!("no such challenge"))?
;
1024 let reg
= self.webauthn_registrations
.remove(index
);
1025 if reg
.is_expired(expire_before
) {
1026 bail
!("no such challenge");
1029 let (credential
, _authenticator
) =
1030 webauthn
.register_credential(&response
, ®
.state
, |id
| -> Result
<bool
, ()> {
1031 Ok(existing_registrations
1033 .any(|cred
| cred
.entry
.cred_id
== *id
))
1036 Ok(TfaEntry
::new(reg
.description
, credential
.into()))