From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH proxmox 4/6] tfa: make configured webauthn origin optional
Date: Fri, 26 Nov 2021 14:55:22 +0100 [thread overview]
Message-ID: <20211126135524.117846-19-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20211126135524.117846-1-w.bumiller@proxmox.com>
and add a webauthn origin override parameter to all methods
accessing it
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
proxmox-tfa/src/api/methods.rs | 21 ++++++++++++---
proxmox-tfa/src/api/mod.rs | 45 +++++++++++++++++++++------------
proxmox-tfa/src/api/webauthn.rs | 45 +++++++++++++++++++++++----------
3 files changed, 79 insertions(+), 32 deletions(-)
diff --git a/proxmox-tfa/src/api/methods.rs b/proxmox-tfa/src/api/methods.rs
index 56971f1..b63d56e 100644
--- a/proxmox-tfa/src/api/methods.rs
+++ b/proxmox-tfa/src/api/methods.rs
@@ -317,6 +317,7 @@ pub fn add_tfa_entry<A: OpenUserChallengeData>(
value: Option<String>,
challenge: Option<String>,
r#type: TfaType,
+ origin: Option<&url::Url>,
) -> Result<TfaUpdateInfo, Error> {
match r#type {
TfaType::Totp => {
@@ -331,7 +332,15 @@ pub fn add_tfa_entry<A: OpenUserChallengeData>(
bail!("'totp' parameter is invalid for 'webauthn' entries");
}
- add_webauthn(config, access, userid, description, challenge, value)
+ add_webauthn(
+ config,
+ access,
+ userid,
+ description,
+ challenge,
+ value,
+ origin,
+ )
}
TfaType::U2f => {
if totp.is_some() {
@@ -436,10 +445,16 @@ fn add_webauthn<A: OpenUserChallengeData>(
description: Option<String>,
challenge: Option<String>,
value: Option<String>,
+ origin: Option<&url::Url>,
) -> Result<TfaUpdateInfo, Error> {
match challenge {
None => config
- .webauthn_registration_challenge(access, &userid, need_description(description)?)
+ .webauthn_registration_challenge(
+ access,
+ &userid,
+ need_description(description)?,
+ origin,
+ )
.map(|c| TfaUpdateInfo {
challenge: Some(c),
..Default::default()
@@ -449,7 +464,7 @@ fn add_webauthn<A: OpenUserChallengeData>(
format_err!("missing 'value' parameter (webauthn challenge response missing)")
})?;
config
- .webauthn_registration_finish(access, &userid, &challenge, &value)
+ .webauthn_registration_finish(access, &userid, &challenge, &value, origin)
.map(TfaUpdateInfo::id)
}
}
diff --git a/proxmox-tfa/src/api/mod.rs b/proxmox-tfa/src/api/mod.rs
index e0084b9..b591a23 100644
--- a/proxmox-tfa/src/api/mod.rs
+++ b/proxmox-tfa/src/api/mod.rs
@@ -9,6 +9,7 @@ use std::collections::HashMap;
use anyhow::{bail, format_err, Error};
use serde::{Deserialize, Serialize};
use serde_json::Value;
+use url::Url;
use webauthn_rs::{proto::UserVerificationPolicy, Webauthn};
@@ -28,6 +29,7 @@ pub mod methods;
pub use recovery::RecoveryState;
pub use u2f::U2fConfig;
+use webauthn::WebauthnConfigInstance;
pub use webauthn::{WebauthnConfig, WebauthnCredential};
#[cfg(feature = "api-types")]
@@ -93,15 +95,22 @@ fn check_u2f(u2f: &Option<U2fConfig>) -> Result<u2f::U2f, Error> {
/// Helper to get a `Webauthn` instance from a `WebauthnConfig`, or `None` if there isn't one
/// configured.
-fn get_webauthn(waconfig: &Option<WebauthnConfig>) -> Option<Webauthn<WebauthnConfig>> {
- waconfig.clone().map(Webauthn::new)
+fn get_webauthn<'a, 'config: 'a, 'origin: 'a>(
+ waconfig: &'config Option<WebauthnConfig>,
+ origin: Option<&'origin Url>,
+) -> Option<Result<Webauthn<WebauthnConfigInstance<'a>>, Error>> {
+ Some(waconfig.as_ref()?.instantiate(origin).map(Webauthn::new))
}
/// Helper to get a u2f instance from a u2f config.
///
/// This is outside of `TfaConfig` to not borrow its `&self`.
-fn check_webauthn(waconfig: &Option<WebauthnConfig>) -> Result<Webauthn<WebauthnConfig>, Error> {
- get_webauthn(waconfig).ok_or_else(|| format_err!("no webauthn configuration available"))
+fn check_webauthn<'a, 'config: 'a, 'origin: 'a>(
+ waconfig: &'config Option<WebauthnConfig>,
+ origin: Option<&'origin Url>,
+) -> Result<Webauthn<WebauthnConfigInstance<'a>>, Error> {
+ get_webauthn(waconfig, origin)
+ .ok_or_else(|| format_err!("no webauthn configuration available"))?
}
impl TfaConfig {
@@ -142,8 +151,9 @@ impl TfaConfig {
access: A,
user: &str,
description: String,
+ origin: Option<&Url>,
) -> Result<String, Error> {
- let webauthn = check_webauthn(&self.webauthn)?;
+ let webauthn = check_webauthn(&self.webauthn, origin)?;
self.users
.entry(user.to_owned())
@@ -158,8 +168,9 @@ impl TfaConfig {
userid: &str,
challenge: &str,
response: &str,
+ origin: Option<&Url>,
) -> Result<String, Error> {
- let webauthn = check_webauthn(&self.webauthn)?;
+ let webauthn = check_webauthn(&self.webauthn, origin)?;
let response: webauthn_rs::proto::RegisterPublicKeyCredential =
serde_json::from_str(response)
@@ -208,12 +219,13 @@ impl TfaConfig {
&mut self,
access: A,
userid: &str,
+ origin: Option<&Url>,
) -> Result<Option<TfaChallenge>, Error> {
match self.users.get_mut(userid) {
Some(udata) => udata.challenge(
access,
userid,
- get_webauthn(&self.webauthn),
+ get_webauthn(&self.webauthn, origin),
get_u2f(&self.u2f).as_ref(),
),
None => Ok(None),
@@ -227,6 +239,7 @@ impl TfaConfig {
userid: &str,
challenge: &TfaChallenge,
response: TfaResponse,
+ origin: Option<&Url>,
) -> Result<NeedsSaving, Error> {
match self.users.get_mut(userid) {
Some(user) => match response {
@@ -239,7 +252,7 @@ impl TfaConfig {
None => bail!("no u2f factor available for user '{}'", userid),
},
TfaResponse::Webauthn(value) => {
- let webauthn = check_webauthn(&self.webauthn)?;
+ let webauthn = check_webauthn(&self.webauthn, origin)?;
user.verify_webauthn(access.clone(), userid, webauthn, value)
}
TfaResponse::Recovery(value) => {
@@ -424,7 +437,7 @@ impl TfaUserData {
fn webauthn_registration_challenge<A: OpenUserChallengeData>(
&mut self,
access: A,
- webauthn: Webauthn<WebauthnConfig>,
+ webauthn: Webauthn<WebauthnConfigInstance>,
userid: &str,
description: String,
) -> Result<String, Error> {
@@ -463,7 +476,7 @@ impl TfaUserData {
fn webauthn_registration_finish<A: OpenUserChallengeData>(
&mut self,
access: A,
- webauthn: Webauthn<WebauthnConfig>,
+ webauthn: Webauthn<WebauthnConfigInstance>,
userid: &str,
challenge: &str,
response: webauthn_rs::proto::RegisterPublicKeyCredential,
@@ -555,11 +568,11 @@ impl TfaUserData {
}
/// Generate a generic TFA challenge. See the [`TfaChallenge`] description for details.
- pub fn challenge<A: OpenUserChallengeData>(
+ fn challenge<A: OpenUserChallengeData>(
&mut self,
access: A,
userid: &str,
- webauthn: Option<Webauthn<WebauthnConfig>>,
+ webauthn: Option<Result<Webauthn<WebauthnConfigInstance>, Error>>,
u2f: Option<&u2f::U2f>,
) -> Result<Option<TfaChallenge>, Error> {
if self.is_empty() {
@@ -570,7 +583,7 @@ impl TfaUserData {
totp: self.totp.iter().any(|e| e.info.enable),
recovery: RecoveryState::from(&self.recovery),
webauthn: match webauthn {
- Some(webauthn) => self.webauthn_challenge(access.clone(), userid, webauthn)?,
+ Some(webauthn) => self.webauthn_challenge(access.clone(), userid, webauthn?)?,
None => None,
},
u2f: match u2f {
@@ -591,7 +604,7 @@ impl TfaUserData {
&mut self,
access: A,
userid: &str,
- webauthn: Webauthn<WebauthnConfig>,
+ webauthn: Webauthn<WebauthnConfigInstance>,
) -> Result<Option<webauthn_rs::proto::RequestChallengeResponse>, Error> {
if self.webauthn.is_empty() {
return Ok(None);
@@ -703,7 +716,7 @@ impl TfaUserData {
&mut self,
access: A,
userid: &str,
- webauthn: Webauthn<WebauthnConfig>,
+ webauthn: Webauthn<WebauthnConfigInstance>,
mut response: Value,
) -> Result<(), Error> {
let expire_before = proxmox_time::epoch_i64() - CHALLENGE_TIMEOUT_SECS;
@@ -995,7 +1008,7 @@ impl TfaUserChallenges {
/// `webauthn_registration_challenge`. The response should come directly from the client.
fn webauthn_registration_finish(
&mut self,
- webauthn: Webauthn<WebauthnConfig>,
+ webauthn: Webauthn<WebauthnConfigInstance>,
challenge: &str,
response: webauthn_rs::proto::RegisterPublicKeyCredential,
existing_registrations: &[TfaEntry<WebauthnCredential>],
diff --git a/proxmox-tfa/src/api/webauthn.rs b/proxmox-tfa/src/api/webauthn.rs
index 486e4f5..aed2885 100644
--- a/proxmox-tfa/src/api/webauthn.rs
+++ b/proxmox-tfa/src/api/webauthn.rs
@@ -1,6 +1,6 @@
//! Webauthn configuration and challenge data.
-use anyhow::Error;
+use anyhow::{format_err, Error};
use serde::{Deserialize, Serialize};
use url::Url;
use webauthn_rs::proto::{COSEKey, Credential, CredentialID, UserVerificationPolicy};
@@ -50,7 +50,7 @@ impl Into<String> for OriginUrl {
#[cfg_attr(feature = "api-types", api(
properties: {
rp: { type: String },
- origin: { type: String },
+ origin: { type: String, optional: true },
id: { type: String },
}
))]
@@ -68,7 +68,8 @@ pub struct WebauthnConfig {
/// users type in their browsers to access the web interface.
///
/// Changing this *may* break existing credentials.
- pub origin: OriginUrl,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub origin: Option<OriginUrl>,
/// Relying party ID. Must be the domain name without protocol, port or location.
///
@@ -78,31 +79,49 @@ pub struct WebauthnConfig {
impl WebauthnConfig {
pub fn digest(&self) -> [u8; 32] {
- let data = format!(
- "rp={:?}\norigin={:?}\nid={:?}\n",
- self.rp,
- self.origin.0.as_str(),
- self.id,
- );
+ let mut data = format!("rp={:?}\nid={:?}\n", self.rp, self.id,);
+ if let Some(origin) = &self.origin {
+ data.push_str(&format!("origin={:?}\n", origin.as_str()));
+ }
openssl::sha::sha256(data.as_bytes())
}
+
+ /// Instantiate a usable webauthn configuration instance.
+ pub(super) fn instantiate<'a, 'this: 'a, 'origin: 'a>(
+ &'this self,
+ origin: Option<&'origin Url>,
+ ) -> Result<WebauthnConfigInstance<'a>, Error> {
+ Ok(WebauthnConfigInstance {
+ origin: origin
+ .or_else(|| self.origin.as_ref().map(|u| &u.0))
+ .ok_or_else(|| format_err!("missing webauthn origin"))?,
+ rp: &self.rp,
+ id: &self.id,
+ })
+ }
+}
+
+pub(super) struct WebauthnConfigInstance<'a> {
+ rp: &'a str,
+ origin: &'a Url,
+ id: &'a str,
}
/// For now we just implement this on the configuration this way.
///
/// Note that we may consider changing this so `get_origin` returns the `Host:` header provided by
/// the connecting client.
-impl webauthn_rs::WebauthnConfig for WebauthnConfig {
+impl<'a> webauthn_rs::WebauthnConfig for WebauthnConfigInstance<'a> {
fn get_relying_party_name(&self) -> &str {
- &self.rp
+ self.rp
}
fn get_origin(&self) -> &Url {
- &self.origin.0
+ self.origin
}
fn get_relying_party_id(&self) -> &str {
- &self.id
+ self.id
}
}
--
2.30.2
next prev parent reply other threads:[~2021-11-26 13:56 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-11-26 13:55 [pmg-devel] [PATCH multiple 0/7] PMG TFA support Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 1/6] add tfa.json and its lock methods Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 2/6] add PMG::TFAConfig module Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 3/6] add TFA API Wolfgang Bumiller
2021-11-26 17:29 ` Stoiko Ivanov
2021-11-26 13:55 ` [pmg-devel] [PATCH api 4/6] add tfa config api Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 5/6] implement tfa authentication Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 6/6] provide qrcode.min.js from libjs-qrcodejs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH gui] add TFA components Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 1/7] pve: bump perlmod to 0.9 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 2/7] pve: update to proxmox-tfa 2.0 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 3/7] pve: bump d/control Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 4/7] import pmg-rs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 5/7] pmg: bump perlmod to 0.9 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 6/7] pmg: add tfa module Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 7/7] pmg: bump d/control Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 1/6] tfa: fix typo in docs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 2/6] tfa: add WebauthnConfig::digest method Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 3/6] tfa: let OriginUrl deref to its inner Url, add FromStr impl Wolfgang Bumiller
2021-11-26 13:55 ` Wolfgang Bumiller [this message]
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 5/6] tfa: clippy fixes Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 6/6] bump proxmox-tfa to 2.0.0-1 Wolfgang Bumiller
2021-11-26 17:34 ` [pmg-devel] [PATCH multiple 0/7] PMG TFA support Stoiko Ivanov
2021-11-28 21:17 ` [pmg-devel] applied-series: " Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20211126135524.117846-19-w.bumiller@proxmox.com \
--to=w.bumiller@proxmox.com \
--cc=pmg-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.