1 use std
::collections
::HashMap
;
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 use std
::time
::Duration
;
9 use anyhow
::{bail, format_err, Error}
;
10 use nix
::sys
::stat
::Mode
;
11 use openssl
::hash
::MessageDigest
;
12 use openssl
::pkey
::PKey
;
13 use openssl
::sign
::Signer
;
14 use serde
::{de::Deserializer, Deserialize, Serialize}
;
15 use serde_json
::Value
;
16 use webauthn_rs
::{proto::UserVerificationPolicy, Webauthn}
;
18 use webauthn_rs
::proto
::Credential
as WebauthnCredential
;
20 use proxmox
::api
::api
;
21 use proxmox
::api
::schema
::Updater
;
22 use proxmox
::sys
::error
::SysError
;
23 use proxmox
::tools
::fs
::CreateOptions
;
24 use proxmox
::tools
::tfa
::totp
::Totp
;
25 use proxmox
::tools
::tfa
::u2f
;
26 use proxmox
::tools
::uuid
::Uuid
;
27 use proxmox
::tools
::AsHex
;
29 use pbs_buildcfg
::configdir
;
31 use crate::api2
::types
::Userid
;
33 /// Mapping of userid to TFA entry.
34 pub type TfaUsers
= HashMap
<Userid
, TfaUserData
>;
36 const CONF_FILE
: &str = configdir
!("/tfa.json");
37 const LOCK_FILE
: &str = configdir
!("/tfa.json.lock");
38 const LOCK_TIMEOUT
: Duration
= Duration
::from_secs(5);
40 const CHALLENGE_DATA_PATH
: &str = pbs_buildcfg
::rundir
!("/tfa/challenges");
42 /// U2F registration challenges time out after 2 minutes.
43 const CHALLENGE_TIMEOUT
: i64 = 2 * 60;
45 pub fn read_lock() -> Result
<File
, Error
> {
46 proxmox
::tools
::fs
::open_file_locked(LOCK_FILE
, LOCK_TIMEOUT
, false)
49 pub fn write_lock() -> Result
<File
, Error
> {
50 proxmox
::tools
::fs
::open_file_locked(LOCK_FILE
, LOCK_TIMEOUT
, true)
53 /// Read the TFA entries.
54 pub fn read() -> Result
<TfaConfig
, Error
> {
55 let file
= match File
::open(CONF_FILE
) {
57 Err(ref err
) if err
.not_found() => return Ok(TfaConfig
::default()),
58 Err(err
) => return Err(err
.into()),
61 Ok(serde_json
::from_reader(file
)?
)
64 /// Get the webauthn config with a digest.
66 /// This is meant only for configuration updates, which currently only means webauthn updates.
67 /// Since this is meant to be done only once (since changes will lock out users), this should be
68 /// used rarely, since the digest calculation is currently a bit more involved.
69 pub fn webauthn_config() -> Result
<Option
<(WebauthnConfig
, [u8; 32])>, Error
>{
70 Ok(match read()?
.webauthn
{
72 let digest
= wa
.digest()?
;
79 /// Requires the write lock to be held.
80 pub fn write(data
: &TfaConfig
) -> Result
<(), Error
> {
81 let options
= CreateOptions
::new().perm(Mode
::from_bits_truncate(0o0600));
83 let json
= serde_json
::to_vec(data
)?
;
84 proxmox
::tools
::fs
::replace_file(CONF_FILE
, &json
, options
)
87 #[derive(Deserialize, Serialize)]
88 pub struct U2fConfig
{
93 #[derive(Clone, Deserialize, Serialize, Updater)]
94 #[serde(deny_unknown_fields)]
95 /// Server side webauthn server configuration.
96 pub struct WebauthnConfig
{
97 /// Relying party name. Any text identifier.
99 /// Changing this *may* break existing credentials.
102 /// Site origin. Must be a `https://` URL (or `http://localhost`). Should contain the address
103 /// users type in their browsers to access the web interface.
105 /// Changing this *may* break existing credentials.
108 /// Relying part ID. Must be the domain name without protocol, port or location.
110 /// Changing this *will* break existing credentials.
114 impl WebauthnConfig
{
115 pub fn digest(&self) -> Result
<[u8; 32], Error
> {
116 let digest_data
= crate::tools
::json
::to_canonical_json(&serde_json
::to_value(self)?
)?
;
117 Ok(openssl
::sha
::sha256(&digest_data
))
121 /// For now we just implement this on the configuration this way.
123 /// Note that we may consider changing this so `get_origin` returns the `Host:` header provided by
124 /// the connecting client.
125 impl webauthn_rs
::WebauthnConfig
for WebauthnConfig
{
126 fn get_relying_party_name(&self) -> String
{
130 fn get_origin(&self) -> &String
{
134 fn get_relying_party_id(&self) -> String
{
139 /// Helper to get a u2f instance from a u2f config, or `None` if there isn't one configured.
140 fn get_u2f(u2f
: &Option
<U2fConfig
>) -> Option
<u2f
::U2f
> {
142 .map(|cfg
| u2f
::U2f
::new(cfg
.appid
.clone(), cfg
.appid
.clone()))
145 /// Helper to get a u2f instance from a u2f config.
147 /// This is outside of `TfaConfig` to not borrow its `&self`.
148 fn check_u2f(u2f
: &Option
<U2fConfig
>) -> Result
<u2f
::U2f
, Error
> {
149 get_u2f(u2f
).ok_or_else(|| format_err
!("no u2f configuration available"))
152 /// Helper to get a `Webauthn` instance from a `WebauthnConfig`, or `None` if there isn't one
154 fn get_webauthn(waconfig
: &Option
<WebauthnConfig
>) -> Option
<Webauthn
<WebauthnConfig
>> {
155 waconfig
.clone().map(Webauthn
::new
)
158 /// Helper to get a u2f instance from a u2f config.
160 /// This is outside of `TfaConfig` to not borrow its `&self`.
161 fn check_webauthn(waconfig
: &Option
<WebauthnConfig
>) -> Result
<Webauthn
<WebauthnConfig
>, Error
> {
162 get_webauthn(waconfig
).ok_or_else(|| format_err
!("no webauthn configuration available"))
165 /// TFA Configuration for this instance.
166 #[derive(Default, Deserialize, Serialize)]
167 pub struct TfaConfig
{
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub u2f
: Option
<U2fConfig
>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub webauthn
: Option
<WebauthnConfig
>,
174 #[serde(skip_serializing_if = "TfaUsers::is_empty", default)]
179 /// Get a two factor authentication challenge for a user, if the user has TFA set up.
180 pub fn login_challenge(&mut self, userid
: &Userid
) -> Result
<Option
<TfaChallenge
>, Error
> {
181 match self.users
.get_mut(userid
) {
182 Some(udata
) => udata
.challenge(
184 get_webauthn(&self.webauthn
),
185 get_u2f(&self.u2f
).as_ref(),
191 /// Get a u2f registration challenge.
192 fn u2f_registration_challenge(
196 ) -> Result
<String
, Error
> {
197 let u2f
= check_u2f(&self.u2f
)?
;
200 .entry(userid
.clone())
202 .u2f_registration_challenge(userid
, &u2f
, description
)
205 /// Finish a u2f registration challenge.
206 fn u2f_registration_finish(
211 ) -> Result
<String
, Error
> {
212 let u2f
= check_u2f(&self.u2f
)?
;
214 match self.users
.get_mut(userid
) {
215 Some(user
) => user
.u2f_registration_finish(userid
, &u2f
, challenge
, response
),
216 None
=> bail
!("no such challenge"),
220 /// Get a webauthn registration challenge.
221 fn webauthn_registration_challenge(
225 ) -> Result
<String
, Error
> {
226 let webauthn
= check_webauthn(&self.webauthn
)?
;
231 .webauthn_registration_challenge(webauthn
, user
, description
)
234 /// Finish a webauthn registration challenge.
235 fn webauthn_registration_finish(
240 ) -> Result
<String
, Error
> {
241 let webauthn
= check_webauthn(&self.webauthn
)?
;
243 let response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
=
244 serde_json
::from_str(response
)
245 .map_err(|err
| format_err
!("error parsing challenge response: {}", err
))?
;
247 match self.users
.get_mut(userid
) {
248 Some(user
) => user
.webauthn_registration_finish(webauthn
, userid
, challenge
, response
),
249 None
=> bail
!("no such challenge"),
253 /// Verify a TFA response.
257 challenge
: &TfaChallenge
,
258 response
: TfaResponse
,
259 ) -> Result
<(), Error
> {
260 match self.users
.get_mut(userid
) {
261 Some(user
) => match response
{
262 TfaResponse
::Totp(value
) => user
.verify_totp(&value
),
263 TfaResponse
::U2f(value
) => match &challenge
.u2f
{
265 let u2f
= check_u2f(&self.u2f
)?
;
266 user
.verify_u2f(u2f
, &challenge
.challenge
, value
)
268 None
=> bail
!("no u2f factor available for user '{}'", userid
),
270 TfaResponse
::Webauthn(value
) => {
271 let webauthn
= check_webauthn(&self.webauthn
)?
;
272 user
.verify_webauthn(userid
, webauthn
, value
)
274 TfaResponse
::Recovery(value
) => user
.verify_recovery(&value
),
276 None
=> bail
!("no 2nd factor available for user '{}'", userid
),
280 /// Remove non-existent users.
281 pub fn cleanup_users(&mut self, config
: &proxmox
::api
::section_config
::SectionConfigData
) {
282 use crate::config
::user
::User
;
284 .retain(|user
, _
| config
.lookup
::<User
>("user", user
.as_str()).is_ok());
287 /// Remove a user. Returns `true` if the user actually existed.
288 pub fn remove_user(&mut self, user
: &Userid
) -> bool
{
289 self.users
.remove(user
).is_some()
294 /// Over the API we only provide this part when querying a user's second factor list.
295 #[derive(Deserialize, Serialize)]
296 #[serde(deny_unknown_fields)]
298 /// The id used to reference this entry.
301 /// User chosen description for this entry.
302 #[serde(skip_serializing_if = "String::is_empty")]
303 pub description
: String
,
305 /// Creation time of this entry as unix epoch.
308 /// Whether this TFA entry is currently enabled.
309 #[serde(skip_serializing_if = "is_default_tfa_enable")]
310 #[serde(default = "default_tfa_enable")]
315 /// For recovery keys we have a fixed entry.
316 pub(crate) fn recovery(created
: i64) -> Self {
318 id
: "recovery".to_string(),
319 description
: String
::new(),
326 /// A TFA entry for a user.
328 /// This simply connects a raw registration to a non optional descriptive text chosen by the user.
329 #[derive(Deserialize, Serialize)]
330 #[serde(deny_unknown_fields)]
331 pub struct TfaEntry
<T
> {
335 /// The actual entry.
339 impl<T
> TfaEntry
<T
> {
340 /// Create an entry with a description. The id will be autogenerated.
341 fn new(description
: String
, entry
: T
) -> Self {
344 id
: Uuid
::generate().to_string(),
347 created
: proxmox
::tools
::time
::epoch_i64(),
355 fn is_expired(&self, at_epoch
: i64) -> bool
;
358 /// A u2f registration challenge.
359 #[derive(Deserialize, Serialize)]
360 #[serde(deny_unknown_fields)]
361 pub struct U2fRegistrationChallenge
{
362 /// JSON formatted challenge string.
365 /// The description chosen by the user for this registration.
368 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
372 impl U2fRegistrationChallenge
{
373 pub fn new(challenge
: String
, description
: String
) -> Self {
377 created
: proxmox
::tools
::time
::epoch_i64(),
382 impl IsExpired
for U2fRegistrationChallenge
{
383 fn is_expired(&self, at_epoch
: i64) -> bool
{
384 self.created
< at_epoch
388 /// A webauthn registration challenge.
389 #[derive(Deserialize, Serialize)]
390 #[serde(deny_unknown_fields)]
391 pub struct WebauthnRegistrationChallenge
{
392 /// Server side registration state data.
393 state
: webauthn_rs
::RegistrationState
,
395 /// While this is basically the content of a `RegistrationState`, the webauthn-rs crate doesn't
396 /// make this public.
399 /// The description chosen by the user for this registration.
402 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
406 impl WebauthnRegistrationChallenge
{
408 state
: webauthn_rs
::RegistrationState
,
416 created
: proxmox
::tools
::time
::epoch_i64(),
421 impl IsExpired
for WebauthnRegistrationChallenge
{
422 fn is_expired(&self, at_epoch
: i64) -> bool
{
423 self.created
< at_epoch
427 /// A webauthn authentication challenge.
428 #[derive(Deserialize, Serialize)]
429 #[serde(deny_unknown_fields)]
430 pub struct WebauthnAuthChallenge
{
431 /// Server side authentication state.
432 state
: webauthn_rs
::AuthenticationState
,
434 /// While this is basically the content of a `AuthenticationState`, the webauthn-rs crate
435 /// doesn't make this public.
438 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
442 impl WebauthnAuthChallenge
{
443 pub fn new(state
: webauthn_rs
::AuthenticationState
, challenge
: String
) -> Self {
447 created
: proxmox
::tools
::time
::epoch_i64(),
452 impl IsExpired
for WebauthnAuthChallenge
{
453 fn is_expired(&self, at_epoch
: i64) -> bool
{
454 self.created
< at_epoch
458 /// Active TFA challenges per user, stored in `CHALLENGE_DATA_PATH`.
459 #[derive(Default, Deserialize, Serialize)]
460 pub struct TfaUserChallenges
{
461 /// Active u2f registration challenges for a user.
463 /// Expired values are automatically filtered out while parsing the tfa configuration file.
464 #[serde(skip_serializing_if = "Vec::is_empty", default)]
465 #[serde(deserialize_with = "filter_expired_challenge")]
466 u2f_registrations
: Vec
<U2fRegistrationChallenge
>,
468 /// Active webauthn registration challenges for a user.
470 /// Expired values are automatically filtered out while parsing the tfa configuration file.
471 #[serde(skip_serializing_if = "Vec::is_empty", default)]
472 #[serde(deserialize_with = "filter_expired_challenge")]
473 webauthn_registrations
: Vec
<WebauthnRegistrationChallenge
>,
475 /// Active webauthn registration challenges for a user.
477 /// Expired values are automatically filtered out while parsing the tfa configuration file.
478 #[serde(skip_serializing_if = "Vec::is_empty", default)]
479 #[serde(deserialize_with = "filter_expired_challenge")]
480 webauthn_auths
: Vec
<WebauthnAuthChallenge
>,
483 /// Container of `TfaUserChallenges` with the corresponding file lock guard.
485 /// TODO: Implement a general file lock guarded struct container in the `proxmox` crate.
486 pub struct TfaUserChallengeData
{
487 inner
: TfaUserChallenges
,
492 impl TfaUserChallengeData
{
493 /// Build the path to the challenge data file for a user.
494 fn challenge_data_path(userid
: &Userid
) -> PathBuf
{
495 PathBuf
::from(format
!("{}/{}", CHALLENGE_DATA_PATH
, userid
))
498 /// Load the user's current challenges with the intent to create a challenge (create the file
499 /// if it does not exist), and keep a lock on the file.
500 fn open(userid
: &Userid
) -> Result
<Self, Error
> {
501 crate::tools
::create_run_dir()?
;
502 let options
= CreateOptions
::new().perm(Mode
::from_bits_truncate(0o0600));
503 proxmox
::tools
::fs
::create_path(CHALLENGE_DATA_PATH
, Some(options
.clone()), Some(options
))
506 "failed to crate challenge data dir {:?}: {}",
512 let path
= Self::challenge_data_path(userid
);
514 let mut file
= std
::fs
::OpenOptions
::new()
521 .map_err(|err
| format_err
!("failed to create challenge file {:?}: {}", path
, err
))?
;
523 proxmox
::tools
::fs
::lock_file(&mut file
, true, None
)?
;
525 // the file may be empty, so read to a temporary buffer first:
526 let mut data
= Vec
::with_capacity(4096);
528 file
.read_to_end(&mut data
).map_err(|err
| {
529 format_err
!("failed to read challenge data for user {}: {}", userid
, err
)
532 let inner
= if data
.is_empty() {
535 serde_json
::from_slice(&data
).map_err(|err
| {
537 "failed to parse challenge data for user {}: {}",
551 /// `open` without creating the file if it doesn't exist, to finish WA authentications.
552 fn open_no_create(userid
: &Userid
) -> Result
<Option
<Self>, Error
> {
553 let path
= Self::challenge_data_path(userid
);
554 let mut file
= match std
::fs
::OpenOptions
::new()
562 Err(err
) if err
.kind() == io
::ErrorKind
::NotFound
=> return Ok(None
),
563 Err(err
) => return Err(err
.into()),
566 proxmox
::tools
::fs
::lock_file(&mut file
, true, None
)?
;
568 let inner
= serde_json
::from_reader(&mut file
).map_err(|err
| {
569 format_err
!("failed to read challenge data for user {}: {}", userid
, err
)
579 /// Rewind & truncate the file for an update.
580 fn rewind(&mut self) -> Result
<(), Error
> {
581 let pos
= self.lock
.seek(SeekFrom
::Start(0))?
;
584 "unexpected result trying to rewind file, position is {}",
589 proxmox
::c_try
!(unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) }
);
594 /// Save the current data. Note that we do not replace the file here since we lock the file
595 /// itself, as it is in `/run`, and the typical error case for this particular situation
596 /// (machine loses power) simply prevents some login, but that'll probably fail anyway for
597 /// other reasons then...
599 /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
600 /// way also unlocks early.
601 fn save(mut self) -> Result
<(), Error
> {
604 serde_json
::to_writer(&mut &self.lock
, &self.inner
).map_err(|err
| {
605 format_err
!("failed to update challenge file {:?}: {}", self.path
, err
)
611 /// Finish a u2f registration. The challenge should correspond to an output of
612 /// `u2f_registration_challenge` (which is a stringified `RegistrationChallenge`). The response
613 /// should come directly from the client.
614 fn u2f_registration_finish(
619 ) -> Result
<TfaEntry
<u2f
::Registration
>, Error
> {
620 let expire_before
= proxmox
::tools
::time
::epoch_i64() - CHALLENGE_TIMEOUT
;
626 .position(|r
| r
.challenge
== challenge
)
627 .ok_or_else(|| format_err
!("no such challenge"))?
;
629 let reg
= &self.inner
.u2f_registrations
[index
];
630 if reg
.is_expired(expire_before
) {
631 bail
!("no such challenge");
634 // the verify call only takes the actual challenge string, so we have to extract it
635 // (u2f::RegistrationChallenge did not always implement Deserialize...)
636 let chobj
: Value
= serde_json
::from_str(challenge
)
637 .map_err(|err
| format_err
!("error parsing original registration challenge: {}", err
))?
;
638 let challenge
= chobj
["challenge"]
640 .ok_or_else(|| format_err
!("invalid registration challenge"))?
;
642 let (mut reg
, description
) = match u2f
.registration_verify(challenge
, response
)?
{
643 None
=> bail
!("verification failed"),
645 let entry
= self.inner
.u2f_registrations
.remove(index
);
646 (reg
, entry
.description
)
650 // we do not care about the attestation certificates, so don't store them
651 reg
.certificate
.clear();
653 Ok(TfaEntry
::new(description
, reg
))
656 /// Finish a webauthn registration. The challenge should correspond to an output of
657 /// `webauthn_registration_challenge`. The response should come directly from the client.
658 fn webauthn_registration_finish(
660 webauthn
: Webauthn
<WebauthnConfig
>,
662 response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
,
663 existing_registrations
: &[TfaEntry
<WebauthnCredential
>],
664 ) -> Result
<TfaEntry
<WebauthnCredential
>, Error
> {
665 let expire_before
= proxmox
::tools
::time
::epoch_i64() - CHALLENGE_TIMEOUT
;
669 .webauthn_registrations
671 .position(|r
| r
.challenge
== challenge
)
672 .ok_or_else(|| format_err
!("no such challenge"))?
;
674 let reg
= self.inner
.webauthn_registrations
.remove(index
);
675 if reg
.is_expired(expire_before
) {
676 bail
!("no such challenge");
680 webauthn
.register_credential(response
, reg
.state
, |id
| -> Result
<bool
, ()> {
681 Ok(existing_registrations
683 .any(|cred
| cred
.entry
.cred_id
== *id
))
686 Ok(TfaEntry
::new(reg
.description
, credential
))
690 /// TFA data for a user.
691 #[derive(Default, Deserialize, Serialize)]
692 #[serde(deny_unknown_fields)]
693 #[serde(rename_all = "kebab-case")]
694 pub struct TfaUserData
{
695 /// Totp keys for a user.
696 #[serde(skip_serializing_if = "Vec::is_empty", default)]
697 pub(crate) totp
: Vec
<TfaEntry
<Totp
>>,
699 /// Registered u2f tokens for a user.
700 #[serde(skip_serializing_if = "Vec::is_empty", default)]
701 pub(crate) u2f
: Vec
<TfaEntry
<u2f
::Registration
>>,
703 /// Registered webauthn tokens for a user.
704 #[serde(skip_serializing_if = "Vec::is_empty", default)]
705 pub(crate) webauthn
: Vec
<TfaEntry
<WebauthnCredential
>>,
707 /// Recovery keys. (Unordered OTP values).
708 #[serde(skip_serializing_if = "Recovery::option_is_empty", default)]
709 pub(crate) recovery
: Option
<Recovery
>,
713 /// Shortcut to get the recovery entry only if it is not empty!
714 pub fn recovery(&self) -> Option
<&Recovery
> {
715 if Recovery
::option_is_empty(&self.recovery
) {
718 self.recovery
.as_ref()
722 /// `true` if no second factors exist
723 pub fn is_empty(&self) -> bool
{
725 && self.u2f
.is_empty()
726 && self.webauthn
.is_empty()
727 && self.recovery().is_none()
730 /// Find an entry by id, except for the "recovery" entry which we're currently treating
732 pub fn find_entry_mut
<'a
>(&'a
mut self, id
: &str) -> Option
<&'a
mut TfaInfo
> {
733 for entry
in &mut self.totp
{
734 if entry
.info
.id
== id
{
735 return Some(&mut entry
.info
);
739 for entry
in &mut self.webauthn
{
740 if entry
.info
.id
== id
{
741 return Some(&mut entry
.info
);
745 for entry
in &mut self.u2f
{
746 if entry
.info
.id
== id
{
747 return Some(&mut entry
.info
);
754 /// Create a u2f registration challenge.
756 /// The description is required at this point already mostly to better be able to identify such
757 /// challenges in the tfa config file if necessary. The user otherwise has no access to this
758 /// information at this point, as the challenge is identified by its actual challenge data
760 fn u2f_registration_challenge(
765 ) -> Result
<String
, Error
> {
766 let challenge
= serde_json
::to_string(&u2f
.registration_challenge()?
)?
;
768 let mut data
= TfaUserChallengeData
::open(userid
)?
;
771 .push(U2fRegistrationChallenge
::new(
780 fn u2f_registration_finish(
786 ) -> Result
<String
, Error
> {
787 let mut data
= TfaUserChallengeData
::open(userid
)?
;
788 let entry
= data
.u2f_registration_finish(u2f
, challenge
, response
)?
;
791 let id
= entry
.info
.id
.clone();
792 self.u2f
.push(entry
);
796 /// Create a webauthn registration challenge.
798 /// The description is required at this point already mostly to better be able to identify such
799 /// challenges in the tfa config file if necessary. The user otherwise has no access to this
800 /// information at this point, as the challenge is identified by its actual challenge data
802 fn webauthn_registration_challenge(
804 mut webauthn
: Webauthn
<WebauthnConfig
>,
807 ) -> Result
<String
, Error
> {
808 let cred_ids
: Vec
<_
> = self
809 .enabled_webauthn_entries()
810 .map(|cred
| cred
.cred_id
.clone())
813 let userid_str
= userid
.to_string();
814 let (challenge
, state
) = webauthn
.generate_challenge_register_options(
815 userid_str
.as_bytes().to_vec(),
819 Some(UserVerificationPolicy
::Discouraged
),
822 let challenge_string
= challenge
.public_key
.challenge
.to_string();
823 let challenge
= serde_json
::to_string(&challenge
)?
;
825 let mut data
= TfaUserChallengeData
::open(userid
)?
;
827 .webauthn_registrations
828 .push(WebauthnRegistrationChallenge
::new(
838 /// Finish a webauthn registration. The challenge should correspond to an output of
839 /// `webauthn_registration_challenge`. The response should come directly from the client.
840 fn webauthn_registration_finish(
842 webauthn
: Webauthn
<WebauthnConfig
>,
845 response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
,
846 ) -> Result
<String
, Error
> {
847 let mut data
= TfaUserChallengeData
::open(userid
)?
;
849 data
.webauthn_registration_finish(webauthn
, challenge
, response
, &self.webauthn
)?
;
852 let id
= entry
.info
.id
.clone();
853 self.webauthn
.push(entry
);
857 /// Generate a generic TFA challenge. See the [`TfaChallenge`] description for details.
861 webauthn
: Option
<Webauthn
<WebauthnConfig
>>,
862 u2f
: Option
<&u2f
::U2f
>,
863 ) -> Result
<Option
<TfaChallenge
>, Error
> {
868 Ok(Some(TfaChallenge
{
869 totp
: self.totp
.iter().any(|e
| e
.info
.enable
),
870 recovery
: RecoveryState
::from(&self.recovery
),
871 webauthn
: match webauthn
{
872 Some(webauthn
) => self.webauthn_challenge(userid
, webauthn
)?
,
876 Some(u2f
) => self.u2f_challenge(u2f
)?
,
882 /// Helper to iterate over enabled totp entries.
883 fn enabled_totp_entries(&self) -> impl Iterator
<Item
= &Totp
> {
886 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
889 /// Helper to iterate over enabled u2f entries.
890 fn enabled_u2f_entries(&self) -> impl Iterator
<Item
= &u2f
::Registration
> {
893 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
896 /// Helper to iterate over enabled u2f entries.
897 fn enabled_webauthn_entries(&self) -> impl Iterator
<Item
= &WebauthnCredential
> {
900 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
903 /// Generate an optional u2f challenge.
904 fn u2f_challenge(&self, u2f
: &u2f
::U2f
) -> Result
<Option
<U2fChallenge
>, Error
> {
905 if self.u2f
.is_empty() {
909 let keys
: Vec
<u2f
::RegisteredKey
> = self
910 .enabled_u2f_entries()
911 .map(|registration
| registration
.key
.clone())
918 Ok(Some(U2fChallenge
{
919 challenge
: u2f
.auth_challenge()?
,
924 /// Generate an optional webauthn challenge.
925 fn webauthn_challenge(
928 mut webauthn
: Webauthn
<WebauthnConfig
>,
929 ) -> Result
<Option
<webauthn_rs
::proto
::RequestChallengeResponse
>, Error
> {
930 if self.webauthn
.is_empty() {
934 let creds
: Vec
<_
> = self.enabled_webauthn_entries().map(Clone
::clone
).collect();
936 if creds
.is_empty() {
940 let (challenge
, state
) = webauthn
941 .generate_challenge_authenticate(creds
, Some(UserVerificationPolicy
::Discouraged
))?
;
942 let challenge_string
= challenge
.public_key
.challenge
.to_string();
943 let mut data
= TfaUserChallengeData
::open(userid
)?
;
946 .push(WebauthnAuthChallenge
::new(state
, challenge_string
));
952 /// Verify a totp challenge. The `value` should be the totp digits as plain text.
953 fn verify_totp(&self, value
: &str) -> Result
<(), Error
> {
954 let now
= std
::time
::SystemTime
::now();
956 for entry
in self.enabled_totp_entries() {
957 if entry
.verify(value
, now
, -1..=1)?
.is_some() {
962 bail
!("totp verification failed");
965 /// Verify a u2f response.
969 challenge
: &u2f
::AuthChallenge
,
971 ) -> Result
<(), Error
> {
972 let response
: u2f
::AuthResponse
= serde_json
::from_value(response
)
973 .map_err(|err
| format_err
!("invalid u2f response: {}", err
))?
;
975 if let Some(entry
) = self
976 .enabled_u2f_entries()
977 .find(|e
| e
.key
.key_handle
== response
.key_handle())
980 .auth_verify_obj(&entry
.public_key
, &challenge
.challenge
, response
)?
987 bail
!("u2f verification failed");
990 /// Verify a webauthn response.
994 mut webauthn
: Webauthn
<WebauthnConfig
>,
996 ) -> Result
<(), Error
> {
997 let expire_before
= proxmox
::tools
::time
::epoch_i64() - CHALLENGE_TIMEOUT
;
999 let challenge
= match response
1001 .ok_or_else(|| format_err
!("invalid response, must be a json object"))?
1002 .remove("challenge")
1003 .ok_or_else(|| format_err
!("missing challenge data in response"))?
1005 Value
::String(s
) => s
,
1006 _
=> bail
!("invalid challenge data in response"),
1009 let response
: webauthn_rs
::proto
::PublicKeyCredential
= serde_json
::from_value(response
)
1010 .map_err(|err
| format_err
!("invalid webauthn response: {}", err
))?
;
1012 let mut data
= match TfaUserChallengeData
::open_no_create(userid
)?
{
1014 None
=> bail
!("no such challenge"),
1021 .position(|r
| r
.challenge
== challenge
)
1022 .ok_or_else(|| format_err
!("no such challenge"))?
;
1024 let challenge
= data
.inner
.webauthn_auths
.remove(index
);
1025 if challenge
.is_expired(expire_before
) {
1026 bail
!("no such challenge");
1029 // we don't allow re-trying the challenge, so make the removal persistent now:
1031 .map_err(|err
| format_err
!("failed to save challenge file: {}", err
))?
;
1033 match webauthn
.authenticate_credential(response
, challenge
.state
)?
{
1034 Some((_cred
, _counter
)) => Ok(()),
1035 None
=> bail
!("webauthn authentication failed"),
1039 /// Verify a recovery key.
1041 /// NOTE: If successful, the key will automatically be removed from the list of available
1042 /// recovery keys, so the configuration needs to be saved afterwards!
1043 fn verify_recovery(&mut self, value
: &str) -> Result
<(), Error
> {
1044 if let Some(r
) = &mut self.recovery
{
1045 if r
.verify(value
)?
{
1049 bail
!("recovery verification failed");
1052 /// Add a new set of recovery keys. There can only be 1 set of keys at a time.
1053 fn add_recovery(&mut self) -> Result
<Vec
<String
>, Error
> {
1054 if self.recovery
.is_some() {
1055 bail
!("user already has recovery keys");
1058 let (recovery
, original
) = Recovery
::generate()?
;
1060 self.recovery
= Some(recovery
);
1066 /// Recovery entries. We use HMAC-SHA256 with a random secret as a salted hash replacement.
1067 #[derive(Deserialize, Serialize)]
1068 pub struct Recovery
{
1069 /// "Salt" used for the key HMAC.
1072 /// Recovery key entries are HMACs of the original data. When used up they will become `None`
1073 /// since the user is presented an enumerated list of codes, so we know the indices of used and
1075 entries
: Vec
<Option
<String
>>,
1077 /// Creation timestamp as a unix epoch.
1082 /// Generate recovery keys and return the recovery entry along with the original string
1084 fn generate() -> Result
<(Self, Vec
<String
>), Error
> {
1085 let mut secret
= [0u8; 8];
1086 proxmox
::sys
::linux
::fill_with_random_data(&mut secret
)?
;
1088 let mut this
= Self {
1089 secret
: AsHex(&secret
).to_string(),
1090 entries
: Vec
::with_capacity(10),
1091 created
: proxmox
::tools
::time
::epoch_i64(),
1094 let mut original
= Vec
::new();
1096 let mut key_data
= [0u8; 80]; // 10 keys of 12 bytes
1097 proxmox
::sys
::linux
::fill_with_random_data(&mut key_data
)?
;
1098 for b
in key_data
.chunks(8) {
1099 let entry
= format
!(
1107 this
.entries
.push(Some(this
.hash(entry
.as_bytes())?
));
1108 original
.push(entry
);
1111 Ok((this
, original
))
1114 /// Perform HMAC-SHA256 on the data and return the result as a hex string.
1115 fn hash(&self, data
: &[u8]) -> Result
<String
, Error
> {
1116 let secret
= PKey
::hmac(self.secret
.as_bytes())
1117 .map_err(|err
| format_err
!("error instantiating hmac key: {}", err
))?
;
1119 let mut signer
= Signer
::new(MessageDigest
::sha256(), &secret
)
1120 .map_err(|err
| format_err
!("error instantiating hmac signer: {}", err
))?
;
1123 .sign_oneshot_to_vec(data
)
1124 .map_err(|err
| format_err
!("error calculating hmac: {}", err
))?
;
1126 Ok(AsHex(&hmac
).to_string())
1129 /// Iterator over available keys.
1130 fn available(&self) -> impl Iterator
<Item
= &str> {
1131 self.entries
.iter().filter_map(Option
::as_deref
)
1134 /// Count the available keys.
1135 fn count_available(&self) -> usize {
1136 self.available().count()
1139 /// Convenience serde method to check if either the option is `None` or the content `is_empty`.
1140 fn option_is_empty(this
: &Option
<Self>) -> bool
{
1142 .map_or(true, |this
| this
.count_available() == 0)
1145 /// Verify a key and remove it. Returns whether the key was valid. Errors on openssl errors.
1146 fn verify(&mut self, key
: &str) -> Result
<bool
, Error
> {
1147 let hash
= self.hash(key
.as_bytes())?
;
1148 for entry
in &mut self.entries
{
1149 if entry
.as_ref() == Some(&hash
) {
1158 /// Serde helper using our `FilteredVecVisitor` to filter out expired entries directly at load
1160 fn filter_expired_challenge
<'de
, D
, T
>(deserializer
: D
) -> Result
<Vec
<T
>, D
::Error
>
1162 D
: Deserializer
<'de
>,
1163 T
: Deserialize
<'de
> + IsExpired
,
1165 let expire_before
= proxmox
::tools
::time
::epoch_i64() - CHALLENGE_TIMEOUT
;
1167 deserializer
.deserialize_seq(crate::tools
::serde_filter
::FilteredVecVisitor
::new(
1168 "a challenge entry",
1169 move |reg
: &T
| !reg
.is_expired(expire_before
),
1174 /// Get an optional TFA challenge for a user.
1175 pub fn login_challenge(userid
: &Userid
) -> Result
<Option
<TfaChallenge
>, Error
> {
1176 let _lock
= write_lock()?
;
1178 let mut data
= read()?
;
1179 Ok(match data
.login_challenge(userid
)?
{
1180 Some(challenge
) => {
1188 /// Add a TOTP entry for a user. Returns the ID.
1189 pub fn add_totp(userid
: &Userid
, description
: String
, value
: Totp
) -> Result
<String
, Error
> {
1190 let _lock
= write_lock();
1191 let mut data
= read()?
;
1192 let entry
= TfaEntry
::new(description
, value
);
1193 let id
= entry
.info
.id
.clone();
1195 .entry(userid
.clone())
1203 /// Add recovery tokens for the user. Returns the token list.
1204 pub fn add_recovery(userid
: &Userid
) -> Result
<Vec
<String
>, Error
> {
1205 let _lock
= write_lock();
1207 let mut data
= read()?
;
1210 .entry(userid
.clone())
1217 /// Add a u2f registration challenge for a user.
1218 pub fn add_u2f_registration(userid
: &Userid
, description
: String
) -> Result
<String
, Error
> {
1219 let _lock
= crate::config
::tfa
::write_lock();
1220 let mut data
= read()?
;
1221 let challenge
= data
.u2f_registration_challenge(userid
, description
)?
;
1226 /// Finish a u2f registration challenge for a user.
1227 pub fn finish_u2f_registration(
1231 ) -> Result
<String
, Error
> {
1232 let _lock
= crate::config
::tfa
::write_lock();
1233 let mut data
= read()?
;
1234 let id
= data
.u2f_registration_finish(userid
, challenge
, response
)?
;
1239 /// Add a webauthn registration challenge for a user.
1240 pub fn add_webauthn_registration(userid
: &Userid
, description
: String
) -> Result
<String
, Error
> {
1241 let _lock
= crate::config
::tfa
::write_lock();
1242 let mut data
= read()?
;
1243 let challenge
= data
.webauthn_registration_challenge(userid
, description
)?
;
1248 /// Finish a webauthn registration challenge for a user.
1249 pub fn finish_webauthn_registration(
1253 ) -> Result
<String
, Error
> {
1254 let _lock
= crate::config
::tfa
::write_lock();
1255 let mut data
= read()?
;
1256 let id
= data
.webauthn_registration_finish(userid
, challenge
, response
)?
;
1261 /// Verify a TFA challenge.
1262 pub fn verify_challenge(
1264 challenge
: &TfaChallenge
,
1265 response
: TfaResponse
,
1266 ) -> Result
<(), Error
> {
1267 let _lock
= crate::config
::tfa
::write_lock();
1268 let mut data
= read()?
;
1269 data
.verify(userid
, challenge
, response
)?
;
1274 /// Used to inform the user about the recovery code status.
1276 /// This contains the available key indices.
1277 #[derive(Clone, Default, Eq, PartialEq, Deserialize, Serialize)]
1278 pub struct RecoveryState(Vec
<usize>);
1280 impl RecoveryState
{
1281 fn is_unavailable(&self) -> bool
{
1286 impl From
<&Option
<Recovery
>> for RecoveryState
{
1287 fn from(r
: &Option
<Recovery
>) -> Self {
1289 Some(r
) => Self::from(r
),
1290 None
=> Self::default(),
1295 impl From
<&Recovery
> for RecoveryState
{
1296 fn from(r
: &Recovery
) -> Self {
1301 .filter_map(|(idx
, key
)| if key
.is_some() { Some(idx) }
else { None }
)
1307 /// When sending a TFA challenge to the user, we include information about what kind of challenge
1308 /// the user may perform. If webauthn credentials are available, a webauthn challenge will be
1310 #[derive(Deserialize, Serialize)]
1311 #[serde(rename_all = "kebab-case")]
1312 pub struct TfaChallenge
{
1313 /// True if the user has TOTP devices.
1316 /// Whether there are recovery keys available.
1317 #[serde(skip_serializing_if = "RecoveryState::is_unavailable", default)]
1318 recovery
: RecoveryState
,
1320 /// If the user has any u2f tokens registered, this will contain the U2F challenge data.
1321 #[serde(skip_serializing_if = "Option::is_none")]
1322 u2f
: Option
<U2fChallenge
>,
1324 /// If the user has any webauthn credentials registered, this will contain the corresponding
1326 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1327 webauthn
: Option
<webauthn_rs
::proto
::RequestChallengeResponse
>,
1330 /// Data used for u2f challenges.
1331 #[derive(Deserialize, Serialize)]
1332 pub struct U2fChallenge
{
1333 /// AppID and challenge data.
1334 challenge
: u2f
::AuthChallenge
,
1336 /// Available tokens/keys.
1337 keys
: Vec
<u2f
::RegisteredKey
>,
1340 /// A user's response to a TFA challenge.
1341 pub enum TfaResponse
{
1348 impl std
::str::FromStr
for TfaResponse
{
1351 fn from_str(s
: &str) -> Result
<Self, Error
> {
1352 Ok(if let Some(totp
) = s
.strip_prefix("totp:") {
1353 TfaResponse
::Totp(totp
.to_string())
1354 } else if let Some(u2f
) = s
.strip_prefix("u2f:") {
1355 TfaResponse
::U2f(serde_json
::from_str(u2f
)?
)
1356 } else if let Some(webauthn
) = s
.strip_prefix("webauthn:") {
1357 TfaResponse
::Webauthn(serde_json
::from_str(webauthn
)?
)
1358 } else if let Some(recovery
) = s
.strip_prefix("recovery:") {
1359 TfaResponse
::Recovery(recovery
.to_string())
1361 bail
!("invalid tfa response");
1366 const fn default_tfa_enable() -> bool
{
1370 const fn is_default_tfa_enable(v
: &bool
) -> bool
{