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
;
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}
;
17 use webauthn_rs
::proto
::Credential
as WebauthnCredential
;
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
;
25 use pbs_buildcfg
::configdir
;
26 use pbs_config
::{open_backup_lockfile, BackupLockGuard}
;
27 use pbs_api_types
::{Userid, User}
;
29 /// Mapping of userid to TFA entry.
30 pub type TfaUsers
= HashMap
<Userid
, TfaUserData
>;
32 const CONF_FILE
: &str = configdir
!("/tfa.json");
33 const LOCK_FILE
: &str = configdir
!("/tfa.json.lock");
35 const CHALLENGE_DATA_PATH
: &str = pbs_buildcfg
::rundir
!("/tfa/challenges");
37 /// U2F registration challenges time out after 2 minutes.
38 const CHALLENGE_TIMEOUT
: i64 = 2 * 60;
40 pub fn read_lock() -> Result
<BackupLockGuard
, Error
> {
41 open_backup_lockfile(LOCK_FILE
, None
, false)
44 pub fn write_lock() -> Result
<BackupLockGuard
, Error
> {
45 open_backup_lockfile(LOCK_FILE
, None
, true)
48 /// Read the TFA entries.
49 pub fn read() -> Result
<TfaConfig
, Error
> {
50 let file
= match File
::open(CONF_FILE
) {
52 Err(ref err
) if err
.not_found() => return Ok(TfaConfig
::default()),
53 Err(err
) => return Err(err
.into()),
56 Ok(serde_json
::from_reader(file
)?
)
59 /// Get the webauthn config with a digest.
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
{
67 let digest
= wa
.digest()?
;
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));
78 let json
= serde_json
::to_vec(data
)?
;
79 proxmox
::tools
::fs
::replace_file(CONF_FILE
, &json
, options
)
82 #[derive(Deserialize, Serialize)]
83 pub struct U2fConfig
{
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.
94 /// Changing this *may* break existing credentials.
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.
100 /// Changing this *may* break existing credentials.
103 /// Relying part ID. Must be the domain name without protocol, port or location.
105 /// Changing this *will* break existing credentials.
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
))
116 /// For now we just implement this on the configuration this way.
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
{
125 fn get_origin(&self) -> &String
{
129 fn get_relying_party_id(&self) -> String
{
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
> {
137 .map(|cfg
| u2f
::U2f
::new(cfg
.appid
.clone(), cfg
.appid
.clone()))
140 /// Helper to get a u2f instance from a u2f config.
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"))
147 /// Helper to get a `Webauthn` instance from a `WebauthnConfig`, or `None` if there isn't one
149 fn get_webauthn(waconfig
: &Option
<WebauthnConfig
>) -> Option
<Webauthn
<WebauthnConfig
>> {
150 waconfig
.clone().map(Webauthn
::new
)
153 /// Helper to get a u2f instance from a u2f config.
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"))
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
>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub webauthn
: Option
<WebauthnConfig
>,
169 #[serde(skip_serializing_if = "TfaUsers::is_empty", default)]
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(
179 get_webauthn(&self.webauthn
),
180 get_u2f(&self.u2f
).as_ref(),
186 /// Get a u2f registration challenge.
187 fn u2f_registration_challenge(
191 ) -> Result
<String
, Error
> {
192 let u2f
= check_u2f(&self.u2f
)?
;
195 .entry(userid
.clone())
197 .u2f_registration_challenge(userid
, &u2f
, description
)
200 /// Finish a u2f registration challenge.
201 fn u2f_registration_finish(
206 ) -> Result
<String
, Error
> {
207 let u2f
= check_u2f(&self.u2f
)?
;
209 match self.users
.get_mut(userid
) {
210 Some(user
) => user
.u2f_registration_finish(userid
, &u2f
, challenge
, response
),
211 None
=> bail
!("no such challenge"),
215 /// Get a webauthn registration challenge.
216 fn webauthn_registration_challenge(
220 ) -> Result
<String
, Error
> {
221 let webauthn
= check_webauthn(&self.webauthn
)?
;
226 .webauthn_registration_challenge(webauthn
, user
, description
)
229 /// Finish a webauthn registration challenge.
230 fn webauthn_registration_finish(
235 ) -> Result
<String
, Error
> {
236 let webauthn
= check_webauthn(&self.webauthn
)?
;
238 let response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
=
239 serde_json
::from_str(response
)
240 .map_err(|err
| format_err
!("error parsing challenge response: {}", err
))?
;
242 match self.users
.get_mut(userid
) {
243 Some(user
) => user
.webauthn_registration_finish(webauthn
, userid
, challenge
, response
),
244 None
=> bail
!("no such challenge"),
248 /// Verify a TFA response.
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
{
260 let u2f
= check_u2f(&self.u2f
)?
;
261 user
.verify_u2f(u2f
, &challenge
.challenge
, value
)
263 None
=> bail
!("no u2f factor available for user '{}'", userid
),
265 TfaResponse
::Webauthn(value
) => {
266 let webauthn
= check_webauthn(&self.webauthn
)?
;
267 user
.verify_webauthn(userid
, webauthn
, value
)
269 TfaResponse
::Recovery(value
) => user
.verify_recovery(&value
),
271 None
=> bail
!("no 2nd factor available for user '{}'", userid
),
275 /// Remove non-existent users.
276 pub fn cleanup_users(&mut self, config
: &proxmox_section_config
::SectionConfigData
) {
278 .retain(|user
, _
| config
.lookup
::<User
>("user", user
.as_str()).is_ok());
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()
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)]
292 /// The id used to reference this entry.
295 /// User chosen description for this entry.
296 #[serde(skip_serializing_if = "String::is_empty")]
297 pub description
: String
,
299 /// Creation time of this entry as unix epoch.
302 /// Whether this TFA entry is currently enabled.
303 #[serde(skip_serializing_if = "is_default_tfa_enable")]
304 #[serde(default = "default_tfa_enable")]
309 /// For recovery keys we have a fixed entry.
310 pub(crate) fn recovery(created
: i64) -> Self {
312 id
: "recovery".to_string(),
313 description
: String
::new(),
320 /// A TFA entry for a user.
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
> {
329 /// The actual entry.
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 {
338 id
: Uuid
::generate().to_string(),
341 created
: proxmox_time
::epoch_i64(),
349 fn is_expired(&self, at_epoch
: i64) -> bool
;
352 /// A u2f registration challenge.
353 #[derive(Deserialize, Serialize)]
354 #[serde(deny_unknown_fields)]
355 pub struct U2fRegistrationChallenge
{
356 /// JSON formatted challenge string.
359 /// The description chosen by the user for this registration.
362 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
366 impl U2fRegistrationChallenge
{
367 pub fn new(challenge
: String
, description
: String
) -> Self {
371 created
: proxmox_time
::epoch_i64(),
376 impl IsExpired
for U2fRegistrationChallenge
{
377 fn is_expired(&self, at_epoch
: i64) -> bool
{
378 self.created
< at_epoch
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
,
389 /// While this is basically the content of a `RegistrationState`, the webauthn-rs crate doesn't
390 /// make this public.
393 /// The description chosen by the user for this registration.
396 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
400 impl WebauthnRegistrationChallenge
{
402 state
: webauthn_rs
::RegistrationState
,
410 created
: proxmox_time
::epoch_i64(),
415 impl IsExpired
for WebauthnRegistrationChallenge
{
416 fn is_expired(&self, at_epoch
: i64) -> bool
{
417 self.created
< at_epoch
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
,
428 /// While this is basically the content of a `AuthenticationState`, the webauthn-rs crate
429 /// doesn't make this public.
432 /// When the challenge was created as unix epoch. They are supposed to be short-lived.
436 impl WebauthnAuthChallenge
{
437 pub fn new(state
: webauthn_rs
::AuthenticationState
, challenge
: String
) -> Self {
441 created
: proxmox_time
::epoch_i64(),
446 impl IsExpired
for WebauthnAuthChallenge
{
447 fn is_expired(&self, at_epoch
: i64) -> bool
{
448 self.created
< at_epoch
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.
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
>,
462 /// Active webauthn registration challenges for a user.
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
>,
469 /// Active webauthn registration challenges for a user.
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
>,
477 /// Container of `TfaUserChallenges` with the corresponding file lock guard.
479 /// TODO: Implement a general file lock guarded struct container in the `proxmox` crate.
480 pub struct TfaUserChallengeData
{
481 inner
: TfaUserChallenges
,
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
))
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
))
500 "failed to crate challenge data dir {:?}: {}",
506 let path
= Self::challenge_data_path(userid
);
508 let mut file
= std
::fs
::OpenOptions
::new()
515 .map_err(|err
| format_err
!("failed to create challenge file {:?}: {}", path
, err
))?
;
517 proxmox
::tools
::fs
::lock_file(&mut file
, true, None
)?
;
519 // the file may be empty, so read to a temporary buffer first:
520 let mut data
= Vec
::with_capacity(4096);
522 file
.read_to_end(&mut data
).map_err(|err
| {
523 format_err
!("failed to read challenge data for user {}: {}", userid
, err
)
526 let inner
= if data
.is_empty() {
529 serde_json
::from_slice(&data
).map_err(|err
| {
531 "failed to parse challenge data for user {}: {}",
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()
556 Err(err
) if err
.kind() == io
::ErrorKind
::NotFound
=> return Ok(None
),
557 Err(err
) => return Err(err
.into()),
560 proxmox
::tools
::fs
::lock_file(&mut file
, true, None
)?
;
562 let inner
= serde_json
::from_reader(&mut file
).map_err(|err
| {
563 format_err
!("failed to read challenge data for user {}: {}", userid
, err
)
573 /// Rewind & truncate the file for an update.
574 fn rewind(&mut self) -> Result
<(), Error
> {
575 let pos
= self.lock
.seek(SeekFrom
::Start(0))?
;
578 "unexpected result trying to rewind file, position is {}",
583 proxmox
::c_try
!(unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) }
);
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...
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
> {
598 serde_json
::to_writer(&mut &self.lock
, &self.inner
).map_err(|err
| {
599 format_err
!("failed to update challenge file {:?}: {}", self.path
, err
)
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(
613 ) -> Result
<TfaEntry
<u2f
::Registration
>, Error
> {
614 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT
;
620 .position(|r
| r
.challenge
== challenge
)
621 .ok_or_else(|| format_err
!("no such challenge"))?
;
623 let reg
= &self.inner
.u2f_registrations
[index
];
624 if reg
.is_expired(expire_before
) {
625 bail
!("no such challenge");
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"]
634 .ok_or_else(|| format_err
!("invalid registration challenge"))?
;
636 let (mut reg
, description
) = match u2f
.registration_verify(challenge
, response
)?
{
637 None
=> bail
!("verification failed"),
639 let entry
= self.inner
.u2f_registrations
.remove(index
);
640 (reg
, entry
.description
)
644 // we do not care about the attestation certificates, so don't store them
645 reg
.certificate
.clear();
647 Ok(TfaEntry
::new(description
, reg
))
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(
654 webauthn
: Webauthn
<WebauthnConfig
>,
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
;
663 .webauthn_registrations
665 .position(|r
| r
.challenge
== challenge
)
666 .ok_or_else(|| format_err
!("no such challenge"))?
;
668 let reg
= self.inner
.webauthn_registrations
.remove(index
);
669 if reg
.is_expired(expire_before
) {
670 bail
!("no such challenge");
674 webauthn
.register_credential(response
, reg
.state
, |id
| -> Result
<bool
, ()> {
675 Ok(existing_registrations
677 .any(|cred
| cred
.entry
.cred_id
== *id
))
680 Ok(TfaEntry
::new(reg
.description
, credential
))
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
>>,
693 /// Registered u2f tokens for a user.
694 #[serde(skip_serializing_if = "Vec::is_empty", default)]
695 pub(crate) u2f
: Vec
<TfaEntry
<u2f
::Registration
>>,
697 /// Registered webauthn tokens for a user.
698 #[serde(skip_serializing_if = "Vec::is_empty", default)]
699 pub(crate) webauthn
: Vec
<TfaEntry
<WebauthnCredential
>>,
701 /// Recovery keys. (Unordered OTP values).
702 #[serde(skip_serializing_if = "Recovery::option_is_empty", default)]
703 pub(crate) recovery
: Option
<Recovery
>,
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
) {
712 self.recovery
.as_ref()
716 /// `true` if no second factors exist
717 pub fn is_empty(&self) -> bool
{
719 && self.u2f
.is_empty()
720 && self.webauthn
.is_empty()
721 && self.recovery().is_none()
724 /// Find an entry by id, except for the "recovery" entry which we're currently treating
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
);
733 for entry
in &mut self.webauthn
{
734 if entry
.info
.id
== id
{
735 return Some(&mut entry
.info
);
739 for entry
in &mut self.u2f
{
740 if entry
.info
.id
== id
{
741 return Some(&mut entry
.info
);
748 /// Create a u2f registration challenge.
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
754 fn u2f_registration_challenge(
759 ) -> Result
<String
, Error
> {
760 let challenge
= serde_json
::to_string(&u2f
.registration_challenge()?
)?
;
762 let mut data
= TfaUserChallengeData
::open(userid
)?
;
765 .push(U2fRegistrationChallenge
::new(
774 fn u2f_registration_finish(
780 ) -> Result
<String
, Error
> {
781 let mut data
= TfaUserChallengeData
::open(userid
)?
;
782 let entry
= data
.u2f_registration_finish(u2f
, challenge
, response
)?
;
785 let id
= entry
.info
.id
.clone();
786 self.u2f
.push(entry
);
790 /// Create a webauthn registration challenge.
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
796 fn webauthn_registration_challenge(
798 mut webauthn
: Webauthn
<WebauthnConfig
>,
801 ) -> Result
<String
, Error
> {
802 let cred_ids
: Vec
<_
> = self
803 .enabled_webauthn_entries()
804 .map(|cred
| cred
.cred_id
.clone())
807 let userid_str
= userid
.to_string();
808 let (challenge
, state
) = webauthn
.generate_challenge_register_options(
809 userid_str
.as_bytes().to_vec(),
813 Some(UserVerificationPolicy
::Discouraged
),
816 let challenge_string
= challenge
.public_key
.challenge
.to_string();
817 let challenge
= serde_json
::to_string(&challenge
)?
;
819 let mut data
= TfaUserChallengeData
::open(userid
)?
;
821 .webauthn_registrations
822 .push(WebauthnRegistrationChallenge
::new(
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(
836 webauthn
: Webauthn
<WebauthnConfig
>,
839 response
: webauthn_rs
::proto
::RegisterPublicKeyCredential
,
840 ) -> Result
<String
, Error
> {
841 let mut data
= TfaUserChallengeData
::open(userid
)?
;
843 data
.webauthn_registration_finish(webauthn
, challenge
, response
, &self.webauthn
)?
;
846 let id
= entry
.info
.id
.clone();
847 self.webauthn
.push(entry
);
851 /// Generate a generic TFA challenge. See the [`TfaChallenge`] description for details.
855 webauthn
: Option
<Webauthn
<WebauthnConfig
>>,
856 u2f
: Option
<&u2f
::U2f
>,
857 ) -> Result
<Option
<TfaChallenge
>, Error
> {
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
)?
,
870 Some(u2f
) => self.u2f_challenge(u2f
)?
,
876 /// Helper to iterate over enabled totp entries.
877 fn enabled_totp_entries(&self) -> impl Iterator
<Item
= &Totp
> {
880 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
883 /// Helper to iterate over enabled u2f entries.
884 fn enabled_u2f_entries(&self) -> impl Iterator
<Item
= &u2f
::Registration
> {
887 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
890 /// Helper to iterate over enabled u2f entries.
891 fn enabled_webauthn_entries(&self) -> impl Iterator
<Item
= &WebauthnCredential
> {
894 .filter_map(|e
| if e
.info
.enable { Some(&e.entry) }
else { None }
)
897 /// Generate an optional u2f challenge.
898 fn u2f_challenge(&self, u2f
: &u2f
::U2f
) -> Result
<Option
<U2fChallenge
>, Error
> {
899 if self.u2f
.is_empty() {
903 let keys
: Vec
<u2f
::RegisteredKey
> = self
904 .enabled_u2f_entries()
905 .map(|registration
| registration
.key
.clone())
912 Ok(Some(U2fChallenge
{
913 challenge
: u2f
.auth_challenge()?
,
918 /// Generate an optional webauthn challenge.
919 fn webauthn_challenge(
922 mut webauthn
: Webauthn
<WebauthnConfig
>,
923 ) -> Result
<Option
<webauthn_rs
::proto
::RequestChallengeResponse
>, Error
> {
924 if self.webauthn
.is_empty() {
928 let creds
: Vec
<_
> = self.enabled_webauthn_entries().map(Clone
::clone
).collect();
930 if creds
.is_empty() {
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
)?
;
940 .push(WebauthnAuthChallenge
::new(state
, challenge_string
));
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();
950 for entry
in self.enabled_totp_entries() {
951 if entry
.verify(value
, now
, -1..=1)?
.is_some() {
956 bail
!("totp verification failed");
959 /// Verify a u2f response.
963 challenge
: &u2f
::AuthChallenge
,
965 ) -> Result
<(), Error
> {
966 let response
: u2f
::AuthResponse
= serde_json
::from_value(response
)
967 .map_err(|err
| format_err
!("invalid u2f response: {}", err
))?
;
969 if let Some(entry
) = self
970 .enabled_u2f_entries()
971 .find(|e
| e
.key
.key_handle
== response
.key_handle())
974 .auth_verify_obj(&entry
.public_key
, &challenge
.challenge
, response
)?
981 bail
!("u2f verification failed");
984 /// Verify a webauthn response.
988 mut webauthn
: Webauthn
<WebauthnConfig
>,
990 ) -> Result
<(), Error
> {
991 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT
;
993 let challenge
= match response
995 .ok_or_else(|| format_err
!("invalid response, must be a json object"))?
997 .ok_or_else(|| format_err
!("missing challenge data in response"))?
999 Value
::String(s
) => s
,
1000 _
=> bail
!("invalid challenge data in response"),
1003 let response
: webauthn_rs
::proto
::PublicKeyCredential
= serde_json
::from_value(response
)
1004 .map_err(|err
| format_err
!("invalid webauthn response: {}", err
))?
;
1006 let mut data
= match TfaUserChallengeData
::open_no_create(userid
)?
{
1008 None
=> bail
!("no such challenge"),
1015 .position(|r
| r
.challenge
== challenge
)
1016 .ok_or_else(|| format_err
!("no such challenge"))?
;
1018 let challenge
= data
.inner
.webauthn_auths
.remove(index
);
1019 if challenge
.is_expired(expire_before
) {
1020 bail
!("no such challenge");
1023 // we don't allow re-trying the challenge, so make the removal persistent now:
1025 .map_err(|err
| format_err
!("failed to save challenge file: {}", err
))?
;
1027 match webauthn
.authenticate_credential(response
, challenge
.state
)?
{
1028 Some((_cred
, _counter
)) => Ok(()),
1029 None
=> bail
!("webauthn authentication failed"),
1033 /// Verify a recovery key.
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
)?
{
1043 bail
!("recovery verification failed");
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");
1052 let (recovery
, original
) = Recovery
::generate()?
;
1054 self.recovery
= Some(recovery
);
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.
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
1069 entries
: Vec
<Option
<String
>>,
1071 /// Creation timestamp as a unix epoch.
1076 /// Generate recovery keys and return the recovery entry along with the original string
1078 fn generate() -> Result
<(Self, Vec
<String
>), Error
> {
1079 let mut secret
= [0u8; 8];
1080 proxmox
::sys
::linux
::fill_with_random_data(&mut secret
)?
;
1082 let mut this
= Self {
1083 secret
: hex
::encode(&secret
).to_string(),
1084 entries
: Vec
::with_capacity(10),
1085 created
: proxmox_time
::epoch_i64(),
1088 let mut original
= Vec
::new();
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],
1098 this
.entries
.push(Some(this
.hash(entry
.as_bytes())?
));
1099 original
.push(entry
);
1102 Ok((this
, original
))
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
))?
;
1110 let mut signer
= Signer
::new(MessageDigest
::sha256(), &secret
)
1111 .map_err(|err
| format_err
!("error instantiating hmac signer: {}", err
))?
;
1114 .sign_oneshot_to_vec(data
)
1115 .map_err(|err
| format_err
!("error calculating hmac: {}", err
))?
;
1117 Ok(hex
::encode(&hmac
))
1120 /// Iterator over available keys.
1121 fn available(&self) -> impl Iterator
<Item
= &str> {
1122 self.entries
.iter().filter_map(Option
::as_deref
)
1125 /// Count the available keys.
1126 fn count_available(&self) -> usize {
1127 self.available().count()
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
{
1133 .map_or(true, |this
| this
.count_available() == 0)
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
) {
1149 /// Serde helper using our `FilteredVecVisitor` to filter out expired entries directly at load
1151 fn filter_expired_challenge
<'de
, D
, T
>(deserializer
: D
) -> Result
<Vec
<T
>, D
::Error
>
1153 D
: Deserializer
<'de
>,
1154 T
: Deserialize
<'de
> + IsExpired
,
1156 let expire_before
= proxmox_time
::epoch_i64() - CHALLENGE_TIMEOUT
;
1158 deserializer
.deserialize_seq(crate::tools
::serde_filter
::FilteredVecVisitor
::new(
1159 "a challenge entry",
1160 move |reg
: &T
| !reg
.is_expired(expire_before
),
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()?
;
1169 let mut data
= read()?
;
1170 Ok(match data
.login_challenge(userid
)?
{
1171 Some(challenge
) => {
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();
1186 .entry(userid
.clone())
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();
1198 let mut data
= read()?
;
1201 .entry(userid
.clone())
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
)?
;
1217 /// Finish a u2f registration challenge for a user.
1218 pub fn finish_u2f_registration(
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
)?
;
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
)?
;
1239 /// Finish a webauthn registration challenge for a user.
1240 pub fn finish_webauthn_registration(
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
)?
;
1252 /// Verify a TFA challenge.
1253 pub fn verify_challenge(
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
)?
;
1265 /// Used to inform the user about the recovery code status.
1267 /// This contains the available key indices.
1268 #[derive(Clone, Default, Eq, PartialEq, Deserialize, Serialize)]
1269 pub struct RecoveryState(Vec
<usize>);
1271 impl RecoveryState
{
1272 fn is_unavailable(&self) -> bool
{
1277 impl From
<&Option
<Recovery
>> for RecoveryState
{
1278 fn from(r
: &Option
<Recovery
>) -> Self {
1280 Some(r
) => Self::from(r
),
1281 None
=> Self::default(),
1286 impl From
<&Recovery
> for RecoveryState
{
1287 fn from(r
: &Recovery
) -> Self {
1292 .filter_map(|(idx
, key
)| if key
.is_some() { Some(idx) }
else { None }
)
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
1301 #[derive(Deserialize, Serialize)]
1302 #[serde(rename_all = "kebab-case")]
1303 pub struct TfaChallenge
{
1304 /// True if the user has TOTP devices.
1307 /// Whether there are recovery keys available.
1308 #[serde(skip_serializing_if = "RecoveryState::is_unavailable", default)]
1309 recovery
: RecoveryState
,
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
>,
1315 /// If the user has any webauthn credentials registered, this will contain the corresponding
1317 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1318 webauthn
: Option
<webauthn_rs
::proto
::RequestChallengeResponse
>,
1321 /// Data used for u2f challenges.
1322 #[derive(Deserialize, Serialize)]
1323 pub struct U2fChallenge
{
1324 /// AppID and challenge data.
1325 challenge
: u2f
::AuthChallenge
,
1327 /// Available tokens/keys.
1328 keys
: Vec
<u2f
::RegisteredKey
>,
1331 /// A user's response to a TFA challenge.
1332 pub enum TfaResponse
{
1339 impl std
::str::FromStr
for TfaResponse
{
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())
1352 bail
!("invalid tfa response");
1357 const fn default_tfa_enable() -> bool
{
1361 const fn is_default_tfa_enable(v
: &bool
) -> bool
{