add proxmox-auth-api crate

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2023-01-31 14:53:43 +01:00
parent a8bd8fca15
commit 5349ae208b
14 changed files with 2298 additions and 47 deletions

View File

@ -2,6 +2,7 @@
members = [
"proxmox-api-macro",
"proxmox-async",
"proxmox-auth-api",
"proxmox-borrow",
"proxmox-compression",
"proxmox-http",
@ -56,6 +57,8 @@ native-tls = "0.2"
nix = "0.26.1"
once_cell = "1.3.1"
openssl = "0.10"
pam = "0.7"
pam-sys = "0.5"
percent-encoding = "2.1"
pin-utils = "0.1.0"
proc-macro2 = "1.0"
@ -82,10 +85,12 @@ proxmox-compression = { version = "0.1.1", path = "proxmox-compression" }
proxmox-http = { version = "0.8.0", path = "proxmox-http" }
proxmox-io = { version = "1.0.0", path = "proxmox-io" }
proxmox-lang = { version = "1.1", path = "proxmox-lang" }
proxmox-rest-server = { version = "0.3.0", path = "proxmox-rest-server" }
proxmox-router = { version = "1.3.1", path = "proxmox-router" }
proxmox-schema = { version = "1.3.6", path = "proxmox-schema" }
proxmox-serde = { version = "0.1.1", path = "proxmox-serde", features = [ "serde_json" ] }
proxmox-sortable-macro = { version = "0.1.2", path = "proxmox-sortable-macro" }
proxmox-sys = { version = "0.4.2", path = "proxmox-sys" }
proxmox-tfa = { version = "2.1.0", path = "proxmox-tfa" }
proxmox-time = { version = "1.1.4", path = "proxmox-time" }
proxmox-uuid = { version = "1.0.1", path = "proxmox-uuid" }

View File

@ -0,0 +1,49 @@
[package]
name = "proxmox-auth-api"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
exclude.workspace = true
description = "Tickets, API and Realm handling"
[dependencies]
anyhow.workspace = true
base64 = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true }
libc = { workspace = true, optional = true }
log = { workspace = true, optional = true }
http = { workspace = true, optional = true }
openssl = { workspace = true, optional = true }
pam = { workspace = true, optional = true }
pam-sys = { workspace = true, optional = true }
percent-encoding = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = [ "derive" ] }
serde_json = { workspace = true, optional = true }
serde_plain = { workspace = true, optional = true }
proxmox-rest-server = { workspace = true, optional = true }
proxmox-router = { workspace = true, optional = true }
proxmox-schema = { workspace = true, optional = true, features = [ "api-macro", "api-types" ] }
proxmox-tfa = { workspace = true, optional = true, features = [ "api" ] }
[features]
default = []
ticket = [ "dep:base64", "dep:percent-encoding", "dep:openssl" ]
api-types = [ "dep:lazy_static", "dep:regex", "dep:serde", "dep:serde_plain", "dep:proxmox-schema" ]
api = [
"api-types",
"ticket",
"dep:http",
"dep:serde_json",
"dep:proxmox-rest-server",
"dep:proxmox-router",
"dep:proxmox-tfa",
]
pam-authenticator = [ "api", "dep:libc", "dep:log", "dep:pam", "dep:pam-sys" ]

View File

@ -0,0 +1,96 @@
//! Test the `Pam` authenticator's 'store_password' implementation.
use std::future::Future;
use std::io::Write;
use std::pin::Pin;
use std::task::{Context, Poll};
use anyhow::{bail, format_err, Error};
use proxmox_auth_api::api::Authenticator;
use proxmox_auth_api::types::Username;
static LOG: PrintLog = PrintLog;
fn main() -> Result<(), Error> {
poll_result_once(run())
}
async fn run() -> Result<(), Error> {
log::set_logger(&LOG).unwrap();
log::set_max_level(log::LevelFilter::Debug);
let mut args = std::env::args().skip(1);
let (username, changepass): (Username, bool) = match args.next() {
None => bail!("missing username or --check parameter"),
Some(ck) if ck == "--check" => (
args.next()
.ok_or_else(|| format_err!("expected username as paramter"))?
.try_into()?,
false,
),
Some(username) => (username.try_into()?, true),
};
let mut stdout = std::io::stdout();
stdout.write_all(b"New password: ")?;
stdout.flush()?;
let mut input = std::io::stdin().lines();
let password = input
.next()
.ok_or_else(|| format_err!("failed to read new password"))??;
let realm = proxmox_auth_api::Pam::new("test");
if changepass {
realm.store_password(&username, &password)?;
} else {
realm.authenticate_user(&username, &password).await?;
}
Ok(())
}
struct PrintLog;
impl log::Log for PrintLog {
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
true
}
fn flush(&self) {
let _ = std::io::stdout().flush();
}
fn log(&self, record: &log::Record<'_>) {
let _ = writeln!(std::io::stdout(), "{}", record.args());
}
}
pub fn poll_result_once<T, R>(mut fut: T) -> Result<R, Error>
where
T: Future<Output = Result<R, Error>>,
{
let waker = std::task::RawWaker::new(std::ptr::null(), &WAKER_VTABLE);
let waker = unsafe { std::task::Waker::from_raw(waker) };
let mut cx = Context::from_waker(&waker);
unsafe {
match Pin::new_unchecked(&mut fut).poll(&mut cx) {
Poll::Pending => bail!("got Poll::Pending synchronous context"),
Poll::Ready(r) => r,
}
}
}
const WAKER_VTABLE: std::task::RawWakerVTable =
std::task::RawWakerVTable::new(forbid_clone, forbid_wake, forbid_wake, ignore_drop);
unsafe fn forbid_clone(_: *const ()) -> std::task::RawWaker {
panic!("tried to clone waker for synchronous task");
}
unsafe fn forbid_wake(_: *const ()) {
panic!("tried to wake synchronous task");
}
unsafe fn ignore_drop(_: *const ()) {}

View File

@ -0,0 +1,298 @@
//! Provides the "/access/ticket" API call.
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use proxmox_router::{http_err, Permission, RpcEnvironment};
use proxmox_schema::{api, api_types::PASSWORD_SCHEMA};
use proxmox_tfa::api::TfaChallenge;
use super::auth_context;
use super::ApiTicket;
use crate::ticket::Ticket;
use crate::types::{Authid, Userid};
#[allow(clippy::large_enum_variant)]
enum AuthResult {
/// Successful authentication which does not require a new ticket.
Success,
/// Successful authentication which requires a ticket to be created.
CreateTicket,
/// A partial ticket which requires a 2nd factor will be created.
Partial(Box<TfaChallenge>),
}
#[api(
input: {
properties: {
username: {
type: Userid,
},
password: {
schema: PASSWORD_SCHEMA,
},
path: {
type: String,
description: "Path for verifying terminal tickets.",
optional: true,
},
privs: {
type: String,
description: "Privilege for verifying terminal tickets.",
optional: true,
},
port: {
type: Integer,
description: "Port for verifying terminal tickets.",
optional: true,
},
"tfa-challenge": {
type: String,
description: "The signed TFA challenge string the user wants to respond to.",
optional: true,
},
},
},
returns: {
properties: {
username: {
type: String,
description: "User name.",
},
ticket: {
type: String,
description: "Auth ticket.",
},
CSRFPreventionToken: {
type: String,
description:
"Cross Site Request Forgery Prevention Token. \
For partial tickets this is the string \"invalid\".",
},
},
},
protected: true,
access: {
permission: &Permission::World,
},
)]
/// Create or verify authentication ticket.
///
/// Returns: An authentication ticket with additional infos.
pub async fn create_ticket(
username: Userid,
password: String,
path: Option<String>,
privs: Option<String>,
port: Option<u16>,
tfa_challenge: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
use proxmox_rest_server::RestEnvironment;
let env: &RestEnvironment = rpcenv
.as_any()
.downcast_ref::<RestEnvironment>()
.ok_or_else(|| format_err!("detected wrong RpcEnvironment type"))?;
match authenticate_user(&username, &password, path, privs, port, tfa_challenge).await {
Ok(AuthResult::Success) => Ok(json!({ "username": username })),
Ok(AuthResult::CreateTicket) => {
let auth_context = auth_context()?;
let api_ticket = ApiTicket::Full(username.clone());
let ticket = Ticket::new(auth_context.auth_prefix(), &api_ticket)?
.sign(auth_context.keyring(), None)?;
let token = assemble_csrf_prevention_token(auth_context.csrf_secret(), &username);
env.log_auth(username.as_str());
Ok(json!({
"username": username,
"ticket": ticket,
"CSRFPreventionToken": token,
}))
}
Ok(AuthResult::Partial(challenge)) => {
let auth_context = auth_context()?;
let api_ticket = ApiTicket::Partial(challenge);
let ticket = Ticket::new(auth_context.auth_prefix(), &api_ticket)?
.sign(auth_context.keyring(), Some(username.as_str()))?;
Ok(json!({
"username": username,
"ticket": ticket,
"CSRFPreventionToken": "invalid",
}))
}
Err(err) => {
env.log_failed_auth(Some(username.to_string()), &err.to_string());
Err(http_err!(UNAUTHORIZED, "permission check failed."))
}
}
}
async fn authenticate_user(
userid: &Userid,
password: &str,
path: Option<String>,
privs: Option<String>,
port: Option<u16>,
tfa_challenge: Option<String>,
) -> Result<AuthResult, Error> {
let auth_context = auth_context()?;
let prefix = auth_context.auth_prefix();
let auth_id = Authid::from(userid.clone());
if !auth_context.auth_id_is_active(&auth_id)? {
bail!("user account disabled or expired.");
}
if let Some(tfa_challenge) = tfa_challenge {
return authenticate_2nd(userid, &tfa_challenge, password);
}
if password.starts_with(prefix) && password.as_bytes().get(prefix.len()).copied() == Some(b':')
{
if let Ok(ticket_userid) = Ticket::<Userid>::parse(password)
.and_then(|ticket| ticket.verify(auth_context.keyring(), prefix, None))
{
if *userid == ticket_userid {
return Ok(AuthResult::CreateTicket);
}
bail!("ticket login failed - wrong userid");
}
} else if let Some(((path, privs), port)) = path.zip(privs).zip(port) {
match auth_context.check_path_ticket(userid, password, path, privs, port)? {
None => (), // no path based tickets supported, just fall through.
Some(true) => return Ok(AuthResult::Success),
Some(false) => bail!("No such privilege"),
}
}
#[allow(clippy::let_unit_value)]
{
let _: () = auth_context
.lookup_realm(userid.realm())
.ok_or_else(|| format_err!("unknown realm {:?}", userid.realm().as_str()))?
.authenticate_user(userid.name(), password)
.await?;
}
Ok(match login_challenge(userid)? {
None => AuthResult::CreateTicket,
Some(challenge) => AuthResult::Partial(Box::new(challenge)),
})
}
fn authenticate_2nd(
userid: &Userid,
challenge_ticket: &str,
response: &str,
) -> Result<AuthResult, Error> {
let auth_context = auth_context()?;
let challenge: Box<TfaChallenge> = Ticket::<ApiTicket>::parse(challenge_ticket)?
.verify_with_time_frame(
auth_context.keyring(),
auth_context.auth_prefix(),
Some(userid.as_str()),
-60..600,
)?
.require_partial()?;
#[allow(clippy::let_unit_value)]
{
let mut tfa_config_lock = auth_context.tfa_config_write_lock()?;
let (locked_config, tfa_config) = tfa_config_lock.config_mut();
if tfa_config
.verify(
locked_config,
userid.as_str(),
&challenge,
response.parse()?,
None,
)?
.needs_saving()
{
tfa_config_lock.save_config()?;
}
}
Ok(AuthResult::CreateTicket)
}
fn login_challenge(userid: &Userid) -> Result<Option<TfaChallenge>, Error> {
let auth_context = auth_context()?;
let mut tfa_config_lock = auth_context.tfa_config_write_lock()?;
let (locked_config, tfa_config) = tfa_config_lock.config_mut();
tfa_config.authentication_challenge(locked_config, userid.as_str(), None)
}
fn assemble_csrf_prevention_token(secret: &[u8], userid: &Userid) -> String {
let epoch = crate::time::epoch_i64();
let digest = compute_csrf_secret_digest(epoch, secret, userid);
format!("{:08X}:{}", epoch, digest)
}
fn compute_csrf_secret_digest(timestamp: i64, secret: &[u8], userid: &Userid) -> String {
let mut hasher = openssl::sha::Sha256::new();
let data = format!("{:08X}:{}:", timestamp, userid);
hasher.update(data.as_bytes());
hasher.update(secret);
base64::encode_config(hasher.finish(), base64::STANDARD_NO_PAD)
}
pub(crate) fn verify_csrf_prevention_token(
secret: &[u8],
userid: &Userid,
token: &str,
min_age: i64,
max_age: i64,
) -> Result<i64, Error> {
verify_csrf_prevention_token_do(secret, userid, token, min_age, max_age)
.map_err(|err| format_err!("invalid csrf token - {}", err))
}
fn verify_csrf_prevention_token_do(
secret: &[u8],
userid: &Userid,
token: &str,
min_age: i64,
max_age: i64,
) -> Result<i64, Error> {
use std::collections::VecDeque;
let mut parts: VecDeque<&str> = token.split(':').collect();
if parts.len() != 2 {
bail!("format error - wrong number of parts.");
}
let timestamp = parts.pop_front().unwrap();
let sig = parts.pop_front().unwrap();
let ttime = i64::from_str_radix(timestamp, 16)
.map_err(|err| format_err!("timestamp format error - {}", err))?;
let digest = compute_csrf_secret_digest(ttime, secret, userid);
if digest != sig {
bail!("invalid signature.");
}
let now = crate::time::epoch_i64();
let age = now - ttime;
if age < min_age {
bail!("timestamp newer than expected.");
}
if age > max_age {
bail!("timestamp too old.");
}
Ok(age)
}

View File

@ -0,0 +1,220 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex;
use anyhow::{format_err, Error};
use percent_encoding::percent_decode_str;
use proxmox_rest_server::{extract_cookie, AuthError};
use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig};
use crate::auth_key::Keyring;
use crate::types::{Authid, RealmRef, Userid, UsernameRef};
mod access;
mod ticket;
use crate::ticket::Ticket;
use access::verify_csrf_prevention_token;
pub use access::{create_ticket, API_METHOD_CREATE_TICKET};
pub use ticket::{ApiTicket, PartialTicket};
/// Authentication realms are used to manage users: authenticate, change password or remove.
pub trait Authenticator {
/// Authenticate a user given a password.
fn authenticate_user<'a>(
&'a self,
username: &'a UsernameRef,
password: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
/// Change a user's password.
fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
/// Remove a user.
fn remove_password(&self, username: &UsernameRef) -> Result<(), Error>;
}
/// This provides access to the available realms and authentication keys.
pub trait AuthContext: Send + Sync {
/// Lookup a realm by name.
fn lookup_realm(&self, realm: &RealmRef) -> Option<Box<dyn Authenticator + Send + Sync>>;
/// Get the current authentication keyring.
fn keyring(&self) -> &Keyring;
/// The auth prefix without the separating colon. Eg. `"PBS"`.
fn auth_prefix(&self) -> &'static str;
/// API token prefix (without the `'='`).
fn auth_token_prefix(&self) -> &'static str;
/// Auth cookie name.
fn auth_cookie_name(&self) -> &'static str;
/// Access the TFA config with an exclusive lock.
fn tfa_config_write_lock(&self) -> Result<Box<dyn LockedTfaConfig>, Error>;
/// Check if a userid is enabled and return a [`UserInformation`] handle.
fn auth_id_is_active(&self, auth_id: &Authid) -> Result<bool, Error>;
/// CSRF prevention token secret data.
fn csrf_secret(&self) -> &[u8];
/// Verify a token secret.
fn verify_token_secret(&self, token_id: &Authid, token_secret: &str) -> Result<(), Error>;
/// Check path based tickets. (Used for terminal tickets).
fn check_path_ticket(
&self,
userid: &Userid,
password: &str,
path: String,
privs: String,
port: u16,
) -> Result<Option<bool>, Error> {
let _ = (userid, password, path, privs, port);
Ok(None)
}
}
/// When verifying TFA challenges we need to be able to update the TFA config without interference
/// from other threads. Similarly, to authenticate with recovery keys, we need to be able to
/// atomically mark them as used.
pub trait LockedTfaConfig {
/// Get mutable access to the [`TfaConfig`] and retain immutable access to `self`.
fn config_mut(&mut self) -> (&dyn OpenUserChallengeData, &mut TfaConfig);
// Save the modified [`TfaConfig`].
//
// The config will have been modified by accessing the
// [`config_mut`](LockedTfaConfig::config_mut()) method.
fn save_config(&mut self) -> Result<(), Error>;
}
static AUTH_CONTEXT: Mutex<Option<&'static dyn AuthContext>> = Mutex::new(None);
/// Configure access to authentication realms and keys.
pub fn set_auth_context(auth_context: &'static dyn AuthContext) {
*AUTH_CONTEXT.lock().unwrap() = Some(auth_context);
}
fn auth_context() -> Result<&'static dyn AuthContext, Error> {
AUTH_CONTEXT
.lock()
.unwrap()
.clone()
.ok_or_else(|| format_err!("no realm access configured"))
}
struct UserAuthData {
ticket: String,
csrf_token: Option<String>,
}
enum AuthData {
User(UserAuthData),
ApiToken(String),
}
pub fn http_check_auth(
headers: &http::HeaderMap,
method: &http::Method,
) -> Result<String, AuthError> {
let auth_context = auth_context()?;
let auth_data = extract_auth_data(auth_context, headers);
match auth_data {
Some(AuthData::User(user_auth_data)) => {
let ticket = user_auth_data.ticket.clone();
let ticket_lifetime = crate::TICKET_LIFETIME;
let userid: Userid = Ticket::<ApiTicket>::parse(&ticket)?
.verify_with_time_frame(
auth_context.keyring(),
auth_context.auth_prefix(),
None,
-300..ticket_lifetime,
)?
.require_full()?;
let auth_id = Authid::from(userid.clone());
if !auth_context.auth_id_is_active(&auth_id)? {
return Err(format_err!("user account disabled or expired.").into());
}
if method != http::Method::GET {
if let Some(csrf_token) = &user_auth_data.csrf_token {
verify_csrf_prevention_token(
auth_context.csrf_secret(),
&userid,
csrf_token,
-300,
ticket_lifetime,
)?;
} else {
return Err(format_err!("missing CSRF prevention token").into());
}
}
Ok(auth_id.to_string())
}
Some(AuthData::ApiToken(api_token)) => {
let mut parts = api_token.splitn(2, ':');
let tokenid = parts
.next()
.ok_or_else(|| format_err!("failed to split API token header"))?;
let tokenid: Authid = tokenid.parse()?;
if !auth_context.auth_id_is_active(&tokenid)? {
return Err(format_err!("user account or token disabled or expired.").into());
}
let tokensecret = parts
.next()
.ok_or_else(|| format_err!("failed to split API token header"))?;
let tokensecret = percent_decode_str(tokensecret)
.decode_utf8()
.map_err(|_| format_err!("failed to decode API token header"))?;
auth_context.verify_token_secret(&tokenid, &tokensecret)?;
Ok(tokenid.to_string())
}
None => Err(AuthError::NoData),
}
}
fn extract_auth_data(
auth_context: &dyn AuthContext,
headers: &http::HeaderMap,
) -> Option<AuthData> {
if let Some(raw_cookie) = headers.get(http::header::COOKIE) {
if let Ok(cookie) = raw_cookie.to_str() {
if let Some(ticket) = extract_cookie(cookie, auth_context.auth_cookie_name()) {
let csrf_token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) {
Some(Ok(v)) => Some(v.to_owned()),
_ => None,
};
return Some(AuthData::User(UserAuthData { ticket, csrf_token }));
}
}
}
let token_prefix = auth_context.auth_token_prefix();
match headers.get(http::header::AUTHORIZATION).map(|v| v.to_str()) {
Some(Ok(v)) => {
if !v.starts_with(token_prefix) {
return None;
}
match v.as_bytes().get(token_prefix.len()).copied() {
Some(b' ') | Some(b'=') => {
Some(AuthData::ApiToken(v[(token_prefix.len() + 1)..].to_owned()))
}
_ => None,
}
}
_ => None,
}
}

View File

@ -0,0 +1,70 @@
//! API side ticket utility.
use std::fmt;
use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
use proxmox_tfa::api::TfaChallenge;
use crate::types::Userid;
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PartialTicket {
#[serde(rename = "u")]
pub userid: Userid,
#[serde(rename = "c")]
pub challenge: TfaChallenge,
}
/// A new ticket struct used in `check_auth` - mostly for better errors than failing to parse the
/// userid ticket content.
pub enum ApiTicket {
Full(Userid),
Partial(Box<TfaChallenge>),
}
impl ApiTicket {
/// Require the ticket to be a full ticket, otherwise error with a meaningful error message.
pub fn require_full(self) -> Result<Userid, Error> {
match self {
ApiTicket::Full(userid) => Ok(userid),
ApiTicket::Partial(_) => bail!("access denied - second login factor required"),
}
}
/// Expect the ticket to contain a tfa challenge, otherwise error with a meaningful error
/// message.
pub fn require_partial(self) -> Result<Box<TfaChallenge>, Error> {
match self {
ApiTicket::Full(_) => bail!("invalid tfa challenge"),
ApiTicket::Partial(challenge) => Ok(challenge),
}
}
}
impl fmt::Display for ApiTicket {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ApiTicket::Full(userid) => fmt::Display::fmt(userid, f),
ApiTicket::Partial(partial) => {
let data = serde_json::to_string(partial).map_err(|_| fmt::Error)?;
write!(f, "!tfa!{}", data)
}
}
}
}
impl std::str::FromStr for ApiTicket {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if let Some(tfa_ticket) = s.strip_prefix("!tfa!") {
Ok(ApiTicket::Partial(serde_json::from_str(tfa_ticket)?))
} else {
Ok(ApiTicket::Full(s.parse()?))
}
}
}

View File

@ -0,0 +1,218 @@
//! Auth key handling.
use anyhow::{bail, format_err, Error};
use openssl::ec::{EcGroup, EcKey};
use openssl::hash::MessageDigest;
use openssl::nid::Nid;
use openssl::pkey::{HasPublic, PKey, PKeyRef, Private, Public};
use openssl::rsa::Rsa;
use openssl::sign::{Signer, Verifier};
/// A private auth key used for API ticket signing and verification.
pub struct PrivateKey {
pub(crate) key: PKey<Private>,
}
/// A private auth key used for API ticket verification.
pub struct PublicKey {
pub(crate) key: PKey<Public>,
}
impl PrivateKey {
/// Generate a new RSA auth key.
pub fn generate_rsa() -> Result<Self, Error> {
let rsa =
Rsa::generate(4096).map_err(|err| format_err!("failed to generate rsa key - {err}"))?;
Ok(Self {
key: PKey::from_rsa(rsa)
.map_err(|err| format_err!("failed to get PKey for rsa key - {err}"))?,
})
}
/// Generate a new EC auth key.
pub fn generate_ec() -> Result<Self, Error> {
let nid = Nid::X9_62_PRIME256V1;
let group = EcGroup::from_curve_name(nid)
.map_err(|err| format_err!("failed to get P-256 group - {err}"))?;
let ec = EcKey::generate(&group)
.map_err(|err| format_err!("failed to generate EC key for testing - {err}"))?;
Ok(Self {
key: PKey::from_ec_key(ec)
.map_err(|err| format_err!("failed to get PKey for EC key - {err}"))?,
})
}
pub fn from_pem(data: &[u8]) -> Result<Self, Error> {
let key = PKey::private_key_from_pem(data)
.map_err(|err| format_err!("failed to decode private key from PEM - {err}"))?;
Ok(Self { key })
}
/// Get the PEM formatted private key *unencrypted*.
pub fn private_key_to_pem(&self) -> Result<Vec<u8>, Error> {
// No PKCS#8 for legacy reasons:
if let Ok(rsa) = self.key.rsa() {
return rsa
.private_key_to_pem()
.map_err(|err| format_err!("failed to encode rsa private key as PEM - {err}"));
}
if let Ok(ec) = self.key.ec_key() {
return ec
.private_key_to_pem()
.map_err(|err| format_err!("failed to encode ec private key as PEM - {err}"));
}
bail!("unexpected key data")
}
/// Get the PEM formatted public key.
pub fn public_key_to_pem(&self) -> Result<Vec<u8>, Error> {
// No PKCS#8 for legacy reasons:
if let Ok(rsa) = self.key.rsa() {
return rsa
.public_key_to_pem()
.map_err(|err| format_err!("failed to encode rsa public key as PEM - {err}"));
}
if let Ok(ec) = self.key.ec_key() {
return ec
.public_key_to_pem()
.map_err(|err| format_err!("failed to encode ec public key as PEM - {err}"));
}
bail!("unexpected key data")
}
/// Get the public key.
pub fn public_key(&self) -> Result<PublicKey, Error> {
PublicKey::from_pem(&self.public_key_to_pem()?)
}
}
impl From<PKey<Private>> for PrivateKey {
fn from(key: PKey<Private>) -> Self {
Self { key }
}
}
impl PublicKey {
pub fn from_pem(data: &[u8]) -> Result<Self, Error> {
let key = PKey::public_key_from_pem(data)
.map_err(|err| format_err!("failed to decode public key from PEM - {err}"))?;
Ok(Self { key })
}
/// Get the PEM formatted public key.
pub fn public_key_to_pem(&self) -> Result<Vec<u8>, Error> {
// No PKCS#8 for legacy reasons:
if let Ok(rsa) = self.key.rsa() {
return rsa
.public_key_to_pem()
.map_err(|err| format_err!("failed to encode rsa public key as PEM - {err}"));
}
if let Ok(ec) = self.key.ec_key() {
return ec
.public_key_to_pem()
.map_err(|err| format_err!("failed to encode ec public key as PEM - {err}"));
}
bail!("unexpected key data")
}
}
impl From<PKey<Public>> for PublicKey {
fn from(key: PKey<Public>) -> Self {
Self { key }
}
}
/// A key ring for authentication.
///
/// This holds one active signing key for new tickets, and optionally multiple public keys for
/// verifying them in order to support key rollover.
pub struct Keyring {
signing_key: Option<PrivateKey>,
public_keys: Vec<PublicKey>,
}
impl Keyring {
pub fn generate_new_rsa() -> Result<Self, Error> {
PrivateKey::generate_rsa().map(Self::with_private_key)
}
pub fn generate_new_ec() -> Result<Self, Error> {
PrivateKey::generate_ec().map(Self::with_private_key)
}
pub fn new() -> Self {
Self {
signing_key: None,
public_keys: Vec::new(),
}
}
pub fn with_public_key(key: PublicKey) -> Self {
Self {
signing_key: None,
public_keys: vec![key],
}
}
pub fn with_private_key(key: PrivateKey) -> Self {
Self {
signing_key: Some(key),
public_keys: Vec::new(),
}
}
pub fn add_public_key(&mut self, key: PublicKey) {
self.public_keys.push(key);
}
pub fn verify(
&self,
digest: MessageDigest,
signature: &[u8],
data: &[u8],
) -> Result<bool, Error> {
fn verify_with<P: HasPublic>(
key: &PKeyRef<P>,
digest: MessageDigest,
signature: &[u8],
data: &[u8],
) -> Result<bool, Error> {
Verifier::new(digest, key)
.map_err(|err| format_err!("failed to create openssl verifier - {err}"))?
.verify_oneshot(signature, data)
.map_err(|err| format_err!("openssl error verifying data - {err}"))
}
if let Some(key) = &self.signing_key {
if verify_with(&key.key, digest, signature, data)? {
return Ok(true);
}
}
for key in &self.public_keys {
if verify_with(&key.key, digest, signature, data)? {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn signer(&self, digest: MessageDigest) -> Result<Signer, Error> {
Signer::new(
digest,
&self
.signing_key
.as_ref()
.ok_or_else(|| format_err!("no private key available for signing"))?
.key,
)
.map_err(|err| format_err!("failed to create openssl signer - {err}"))
}
}

View File

@ -0,0 +1,36 @@
//! Authentication API crate.
//!
//! This contains the API types for `Userid`/`Realm`/`Authid` etc., the PAM authenticator and the
//! authentication API calls.
//!
//! Each can be enabled via a feature:
//!
//! The `pam-authenticator` feature enables the `Pam` type.
pub const TICKET_LIFETIME: i64 = 3600 * 2; // 2 hours
#[cfg(feature = "ticket")]
mod time;
#[cfg(feature = "api")]
pub mod api;
#[cfg(feature = "api")]
pub use api::set_auth_context;
#[cfg(any(feature = "api", feature = "ticket"))]
mod auth_key;
#[cfg(any(feature = "api", feature = "ticket"))]
pub use auth_key::{Keyring, PrivateKey, PublicKey};
#[cfg(feature = "ticket")]
pub mod ticket;
#[cfg(feature = "api-types")]
pub mod types;
#[cfg(feature = "pam-authenticator")]
mod pam_authenticator;
#[cfg(feature = "pam-authenticator")]
pub use pam_authenticator::Pam;

View File

@ -0,0 +1,193 @@
use std::ffi::{c_int, c_void, CStr};
use std::future::Future;
use std::pin::Pin;
use anyhow::{bail, Error};
use pam_sys::types::{PamHandle, PamMessage, PamMessageStyle, PamResponse, PamReturnCode};
use crate::types::UsernameRef;
#[allow(clippy::upper_case_acronyms)]
pub struct Pam {
service: &'static str,
}
impl Pam {
pub const fn new(service: &'static str) -> Self {
Self { service }
}
}
impl crate::api::Authenticator for Pam {
fn authenticate_user<'a>(
&'a self,
username: &'a UsernameRef,
password: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
Box::pin(async move {
let mut auth = pam::Authenticator::with_password(self.service).unwrap();
auth.get_handler()
.set_credentials(username.as_str(), password);
auth.authenticate()?;
Ok(())
})
}
fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error> {
let mut password_conv = PasswordConv {
login: username.as_str(),
password,
};
let conv = pam_sys::types::PamConversation {
conv: Some(conv_fn),
data_ptr: &mut password_conv as *mut _ as *mut c_void,
};
let mut handle = std::ptr::null_mut();
let err =
pam_sys::wrapped::start(self.service, Some(username.as_str()), &conv, &mut handle);
if err != PamReturnCode::SUCCESS {
bail!("error opening pam - {err}");
}
let mut handle = PamGuard {
handle: unsafe { &mut *handle },
result: PamReturnCode::SUCCESS,
};
/*
* we assume we're root and don't need to authenticate
handle.result =
pam_sys::wrapped::authenticate(handle.handle, pam_sys::types::PamFlag::NONE);
if handle.result != PamReturnCode::SUCCESS {
bail!("authentication error - {err}");
}
handle.result = pam_sys::wrapped::acct_mgmt(handle.handle, pam_sys::types::PamFlag::NONE);
if handle.result != PamReturnCode::SUCCESS {
bail!("account error - {}", handle.result);
}
*/
handle.result = pam_sys::wrapped::chauthtok(handle.handle, pam_sys::types::PamFlag::NONE);
if handle.result != PamReturnCode::SUCCESS {
bail!("error changing auth token - {}", handle.result);
}
Ok(())
}
// do not remove password for pam users
fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
Ok(())
}
}
extern "C" fn conv_fn(
num_messages: c_int,
messages: *mut *mut PamMessage,
responses_out: *mut *mut PamResponse,
data_ptr: *mut c_void,
) -> c_int {
let messages: &[&PamMessage] = unsafe {
std::slice::from_raw_parts(
messages as *const *const PamMessage as *const &PamMessage,
num_messages as usize,
)
};
let mut responses = Vec::new();
responses.resize(
messages.len(),
PamResponse {
resp: std::ptr::null_mut(),
resp_retcode: 0,
},
);
let mut responses = responses.into_boxed_slice();
let data_ptr = unsafe { &*(data_ptr as *const PasswordConv<'_>) };
match data_ptr.converse(messages, &mut responses) {
Ok(()) => {
unsafe {
std::ptr::write(responses_out, &mut Box::leak(responses)[0]);
}
PamReturnCode::SUCCESS as c_int
}
Err(err) => {
log::error!("error conversing with pam - {err}");
PamReturnCode::ABORT as c_int
}
}
}
struct PamGuard<'a> {
handle: &'a mut PamHandle,
result: PamReturnCode,
}
impl Drop for PamGuard<'_> {
fn drop(&mut self) {
pam_sys::wrapped::end(&mut self.handle, self.result);
}
}
struct PasswordConv<'a> {
login: &'a str,
password: &'a str,
}
impl PasswordConv<'_> {
fn converse(
&self,
messages: &[&PamMessage],
responses: &mut [PamResponse],
) -> Result<(), Error> {
for i in 0..messages.len() {
self.msg(messages[i], &mut responses[i])?;
}
Ok(())
}
fn msg(
&self,
msg: &pam_sys::types::PamMessage,
response: &mut PamResponse,
) -> Result<(), Error> {
let resp = match PamMessageStyle::from(msg.msg_style) {
PamMessageStyle::PROMPT_ECHO_ON => {
//let msg = unsafe { CStr::from_ptr(msg.msg) };
//log::info!("pam prompt: {msg:?}");
self.login
}
PamMessageStyle::PROMPT_ECHO_OFF => {
//let msg = unsafe { CStr::from_ptr(msg.msg) };
//log::info!("pam password prompt: {msg:?}");
self.password
}
PamMessageStyle::ERROR_MSG => {
let msg = unsafe { CStr::from_ptr(msg.msg) };
log::error!("pam error: {msg:?}");
return Ok(());
}
PamMessageStyle::TEXT_INFO => {
let msg = unsafe { CStr::from_ptr(msg.msg) };
log::info!("pam message: {msg:?}");
return Ok(());
}
};
// Since CString::into_raw is technically not `free()`-safe...
let resp = resp.as_bytes();
let c_resp = unsafe { libc::malloc(resp.len() + 1) as *mut u8 };
if c_resp.is_null() {
bail!("failed to allocate response");
}
let c_resp = unsafe { std::slice::from_raw_parts_mut(c_resp, resp.len() + 1) };
c_resp[c_resp.len() - 1] = 0;
c_resp[..resp.len()].copy_from_slice(resp);
response.resp = c_resp.as_mut_ptr() as *mut libc::c_char;
Ok(())
}
}

View File

@ -0,0 +1,334 @@
//! Generate and verify Authentication tickets
use std::borrow::Cow;
use std::marker::PhantomData;
use anyhow::{bail, format_err, Error};
use openssl::hash::MessageDigest;
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet};
use crate::auth_key::Keyring;
use crate::TICKET_LIFETIME;
/// Stringified ticket data must not contain colons...
const TICKET_ASCIISET: &AsciiSet = &percent_encoding::CONTROLS.add(b':');
/// An empty type implementing [`ToString`] and [`FromStr`](std::str::FromStr), used for tickets
/// with no data.
pub struct Empty;
impl ToString for Empty {
fn to_string(&self) -> String {
String::new()
}
}
impl std::str::FromStr for Empty {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if !s.is_empty() {
bail!("unexpected ticket data, should be empty");
}
Ok(Empty)
}
}
/// An API ticket consists of a ticket type (prefix), type-dependent data, optional additional
/// authenticaztion data, a timestamp and a signature. We store these values in the form
/// `<prefix>:<stringified data>:<timestamp>::<signature>`.
///
/// The signature is made over the string consisting of prefix, data, timestamp and aad joined
/// together by colons. If there is no additional authentication data it will be skipped together
/// with the colon separating it from the timestamp.
pub struct Ticket<T>
where
T: ToString + std::str::FromStr,
{
prefix: Cow<'static, str>,
data: String,
time: i64,
signature: Option<Vec<u8>>,
_type_marker: PhantomData<fn() -> T>,
}
impl<T> Ticket<T>
where
T: ToString + std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
/// Prepare a new ticket for signing.
pub fn new(prefix: &'static str, data: &T) -> Result<Self, Error> {
Ok(Self {
prefix: Cow::Borrowed(prefix),
data: data.to_string(),
time: crate::time::epoch_i64(),
signature: None,
_type_marker: PhantomData,
})
}
/// Get the ticket prefix.
pub fn prefix(&self) -> &str {
&self.prefix
}
/// Get the ticket's time stamp in seconds since the unix epoch.
pub fn time(&self) -> i64 {
self.time
}
/// Get the raw string data contained in the ticket. The `verify` method will call `parse()`
/// this in the end, so using this method directly is discouraged as it does not verify the
/// signature.
pub fn raw_data(&self) -> &str {
&self.data
}
/// Serialize the ticket into a string.
fn ticket_data(&self) -> String {
format!(
"{}:{}:{:08X}",
percent_encode(self.prefix.as_bytes(), TICKET_ASCIISET),
percent_encode(self.data.as_bytes(), TICKET_ASCIISET),
self.time,
)
}
/// Serialize the verification data.
fn verification_data(&self, aad: Option<&str>) -> Vec<u8> {
let mut data = self.ticket_data().into_bytes();
if let Some(aad) = aad {
data.push(b':');
data.extend(aad.as_bytes());
}
data
}
/// Change the ticket's time, used mostly for testing.
#[cfg(test)]
fn change_time(&mut self, time: i64) -> &mut Self {
self.time = time;
self
}
/// Sign the ticket.
pub fn sign(&mut self, keyring: &Keyring, aad: Option<&str>) -> Result<String, Error> {
let mut output = self.ticket_data();
let mut signer = keyring.signer(MessageDigest::sha256())?;
signer
.update(output.as_bytes())
.map_err(Error::from)
.and_then(|()| {
if let Some(aad) = aad {
signer
.update(b":")
.and_then(|()| signer.update(aad.as_bytes()))
.map_err(Error::from)
} else {
Ok::<_, Error>(())
}
})
.map_err(|err| format_err!("error signing ticket: {}", err))?;
let signature = signer
.sign_to_vec()
.map_err(|err| format_err!("error finishing ticket signature: {}", err))?;
use std::fmt::Write;
write!(
&mut output,
"::{}",
base64::encode_config(&signature, base64::STANDARD_NO_PAD),
)?;
self.signature = Some(signature);
Ok(output)
}
/// `verify` with an additional time frame parameter, not usually required since we always use
/// the same time frame.
pub fn verify_with_time_frame(
&self,
keyring: &Keyring,
prefix: &str,
aad: Option<&str>,
time_frame: std::ops::Range<i64>,
) -> Result<T, Error> {
if self.prefix != prefix {
bail!("ticket with invalid prefix");
}
let signature = match self.signature.as_deref() {
Some(sig) => sig,
None => bail!("invalid ticket without signature"),
};
let age = crate::time::epoch_i64() - self.time;
if age < time_frame.start {
bail!("invalid ticket - timestamp newer than expected");
}
if age > time_frame.end {
bail!("invalid ticket - expired");
}
let is_valid = keyring.verify(
MessageDigest::sha256(),
&signature,
&self.verification_data(aad),
)?;
if !is_valid {
bail!("ticket with invalid signature");
}
self.data
.parse()
.map_err(|err| format_err!("failed to parse contained ticket data: {:?}", err))
}
/// Verify the ticket with the provided key pair. The additional authentication data needs to
/// match the one used when generating the ticket, and the ticket's age must fall into the time
/// frame.
pub fn verify(&self, keyring: &Keyring, prefix: &str, aad: Option<&str>) -> Result<T, Error> {
self.verify_with_time_frame(keyring, prefix, aad, -300..TICKET_LIFETIME)
}
/// Parse a ticket string.
pub fn parse(ticket: &str) -> Result<Self, Error> {
let mut parts = ticket.splitn(4, ':');
let prefix = percent_decode_str(
parts
.next()
.ok_or_else(|| format_err!("ticket without prefix"))?,
)
.decode_utf8()
.map_err(|err| format_err!("invalid ticket, error decoding prefix: {}", err))?;
let data = percent_decode_str(
parts
.next()
.ok_or_else(|| format_err!("ticket without data"))?,
)
.decode_utf8()
.map_err(|err| format_err!("invalid ticket, error decoding data: {}", err))?;
let time = i64::from_str_radix(
parts
.next()
.ok_or_else(|| format_err!("ticket without timestamp"))?,
16,
)
.map_err(|err| format_err!("ticket with bad timestamp: {}", err))?;
let remainder = parts
.next()
.ok_or_else(|| format_err!("ticket without signature"))?;
// <prefix>:<data>:<time>::signature - the 4th `.next()` swallows the first colon in the
// double-colon!
if !remainder.starts_with(':') {
bail!("ticket without signature separator");
}
let signature = base64::decode_config(&remainder[1..], base64::STANDARD_NO_PAD)
.map_err(|err| format_err!("ticket with bad signature: {}", err))?;
Ok(Self {
prefix: Cow::Owned(prefix.into_owned()),
data: data.into_owned(),
time,
signature: Some(signature),
_type_marker: PhantomData,
})
}
}
#[cfg(test)]
mod test {
use std::convert::Infallible;
use std::fmt;
use crate::auth_key::Keyring;
use super::Ticket;
#[derive(Debug, Eq, PartialEq)]
struct Testid(String);
impl std::str::FromStr for Testid {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Infallible> {
Ok(Self(s.to_string()))
}
}
impl fmt::Display for Testid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
fn simple_test<F>(keyring: &Keyring, aad: Option<&str>, modify: F)
where
F: FnOnce(&mut Ticket<Testid>) -> bool,
{
let userid = Testid("root".to_string());
let mut ticket = Ticket::new("PREFIX", &userid).expect("failed to create Ticket struct");
let should_work = modify(&mut ticket);
let ticket = ticket
.sign(keyring, aad)
.expect("failed to sign test ticket");
let parsed =
Ticket::<Testid>::parse(&ticket).expect("failed to parse generated test ticket");
if should_work {
let check: Testid = parsed
.verify(keyring, "PREFIX", aad)
.expect("failed to verify test ticket");
assert_eq!(userid, check);
} else {
parsed
.verify(keyring, "PREFIX", aad)
.expect_err("failed to verify test ticket");
}
}
#[test]
fn test_tickets() {
// first we need keys, for testing we use small keys for speed...
let keyring = Keyring::generate_new_rsa().expect("failed to generate RSA key for testing");
simple_test(&keyring, Some("secret aad data"), |_| true);
simple_test(&keyring, None, |_| true);
simple_test(&keyring, None, |t| {
t.change_time(0);
false
});
simple_test(&keyring, None, |t| {
t.change_time(crate::time::epoch_i64() + 0x1000_0000);
false
});
}
#[test]
fn test_tickets_ecdsa() {
let keyring = Keyring::generate_new_ec().expect("failed to generate EC key for testing");
simple_test(&keyring, Some("secret aad data"), |_| true);
simple_test(&keyring, None, |_| true);
simple_test(&keyring, None, |t| {
t.change_time(0);
false
});
simple_test(&keyring, None, |t| {
t.change_time(crate::time::epoch_i64() + 0x1000_0000);
false
});
}
}

View File

@ -0,0 +1,12 @@
use std::time::{SystemTime, UNIX_EPOCH};
/// Unix epoch.
pub fn epoch_i64() -> i64 {
let now = SystemTime::now();
if now > UNIX_EPOCH {
i64::try_from(now.duration_since(UNIX_EPOCH).unwrap().as_secs()).expect("epoch > 64 bit")
} else {
-i64::try_from(UNIX_EPOCH.duration_since(now).unwrap().as_secs()).expect("epoch > 64 bit")
}
}

View File

@ -0,0 +1,724 @@
//! Types for user handling.
//!
//! We have [`Username`]s, [`Realm`]s and [`Tokenname`]s. To uniquely identify a user/API token, they
//! must be combined into a [`Userid`] or [`Authid`].
//!
//! Since they're all string types, they're organized as follows:
//!
//! * [`Username`]: an owned user name. Internally a `String`.
//! * [`UsernameRef`]: a borrowed user name. Pairs with a `Username` the same way a `str` pairs
//! with `String`, meaning you can only make references to it.
//! * [`Realm`]: an owned realm (`String` equivalent).
//! * [`RealmRef`]: a borrowed realm (`str` equivalent).
//! * [`Tokenname`]: an owned API token name (`String` equivalent)
//! * [`TokennameRef`]: a borrowed `Tokenname` (`str` equivalent).
//! * [`Userid`]: an owned user id (`"user@realm"`).
//! * [`Authid`]: an owned Authentication ID (a `Userid` with an optional `Tokenname`).
//! Note that `Userid` and `Authid` do not have a separate borrowed type.
//!
//! Note that `Username`s are not unique, therefore they do not implement `Eq` and cannot be
//! compared directly. If a direct comparison is really required, they can be compared as strings
//! via the `as_str()` method. [`Realm`]s, [`Userid`]s and [`Authid`]s on the other hand can be
//! compared with each other, as in those cases the comparison has meaning.
use std::borrow::Borrow;
use std::fmt;
use anyhow::{bail, format_err, Error};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use proxmox_schema::{
api, const_regex, ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType,
};
// we only allow a limited set of characters
// colon is not allowed, because we store usernames in
// colon separated lists)!
// slash is not allowed because it is used as pve API delimiter
// also see "man useradd"
#[macro_export]
macro_rules! USER_NAME_REGEX_STR {
() => {
r"(?:[^\s:/[:cntrl:]]+)"
};
}
#[macro_export]
macro_rules! GROUP_NAME_REGEX_STR {
() => {
$crate::USER_NAME_REGEX_STR!()
};
}
#[macro_export]
macro_rules! TOKEN_NAME_REGEX_STR {
() => {
proxmox_schema::SAFE_ID_REGEX_STR!()
};
}
#[macro_export]
macro_rules! USER_ID_REGEX_STR {
() => {
concat!(
$crate::USER_NAME_REGEX_STR!(),
r"@",
proxmox_schema::SAFE_ID_REGEX_STR!()
)
};
}
#[macro_export]
macro_rules! APITOKEN_ID_REGEX_STR {
() => {
concat!(
$crate::USER_ID_REGEX_STR!(),
r"!",
$crate::TOKEN_NAME_REGEX_STR!()
)
};
}
const_regex! {
pub PROXMOX_USER_NAME_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"$");
pub PROXMOX_TOKEN_NAME_REGEX = concat!(r"^", TOKEN_NAME_REGEX_STR!(), r"$");
pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_ID_REGEX_STR!(), r"$");
pub PROXMOX_APITOKEN_ID_REGEX = concat!(r"^", APITOKEN_ID_REGEX_STR!(), r"$");
pub PROXMOX_AUTH_ID_REGEX = concat!(r"^", r"(?:", USER_ID_REGEX_STR!(), r"|", APITOKEN_ID_REGEX_STR!(), r")$");
pub PROXMOX_GROUP_ID_REGEX = concat!(r"^", GROUP_NAME_REGEX_STR!(), r"$");
}
pub const PROXMOX_USER_NAME_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_USER_NAME_REGEX);
pub const PROXMOX_TOKEN_NAME_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_TOKEN_NAME_REGEX);
pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX);
pub const PROXMOX_TOKEN_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_APITOKEN_ID_REGEX);
pub const PROXMOX_AUTH_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_AUTH_ID_REGEX);
pub const PROXMOX_TOKEN_ID_SCHEMA: Schema = StringSchema::new("API Token ID")
.format(&PROXMOX_TOKEN_ID_FORMAT)
.min_length(3)
.max_length(64)
.schema();
pub const PROXMOX_TOKEN_NAME_SCHEMA: Schema = StringSchema::new("API Token name")
.format(&PROXMOX_TOKEN_NAME_FORMAT)
.min_length(3)
.max_length(64)
.schema();
pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX);
pub const PROXMOX_GROUP_ID_SCHEMA: Schema = StringSchema::new("Group ID")
.format(&PROXMOX_GROUP_ID_FORMAT)
.min_length(3)
.max_length(64)
.schema();
pub const PROXMOX_AUTH_REALM_STRING_SCHEMA: StringSchema =
StringSchema::new("Authentication domain ID")
.format(&proxmox_schema::api_types::SAFE_ID_FORMAT)
.min_length(3)
.max_length(32);
pub const PROXMOX_AUTH_REALM_SCHEMA: Schema = PROXMOX_AUTH_REALM_STRING_SCHEMA.schema();
#[api(
type: String,
format: &PROXMOX_USER_NAME_FORMAT,
min_length: 1,
)]
/// The user name part of a user id.
///
/// This alone does NOT uniquely identify the user and therefore does not implement `Eq`. In order
/// to compare user names directly, they need to be explicitly compared as strings by calling
/// `.as_str()`.
///
/// ```compile_fail
/// fn test(a: Username, b: Username) -> bool {
/// a == b // illegal and does not compile
/// }
/// ```
#[derive(Clone, Debug, Hash, Deserialize, Serialize)]
pub struct Username(String);
/// A reference to a user name part of a user id. This alone does NOT uniquely identify the user.
///
/// This is like a `str` to the `String` of a [`Username`].
#[derive(Debug, Hash)]
pub struct UsernameRef(str);
impl UsernameRef {
fn new(s: &str) -> &Self {
unsafe { &*(s as *const str as *const UsernameRef) }
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for Username {
type Target = UsernameRef;
fn deref(&self) -> &UsernameRef {
self.borrow()
}
}
impl Borrow<UsernameRef> for Username {
fn borrow(&self) -> &UsernameRef {
UsernameRef::new(self.0.as_str())
}
}
impl AsRef<UsernameRef> for Username {
fn as_ref(&self) -> &UsernameRef {
self.borrow()
}
}
impl ToOwned for UsernameRef {
type Owned = Username;
fn to_owned(&self) -> Self::Owned {
Username(self.0.to_owned())
}
}
impl TryFrom<String> for Username {
type Error = Error;
fn try_from(s: String) -> Result<Self, Error> {
if !PROXMOX_USER_NAME_REGEX.is_match(&s) {
bail!("invalid user name");
}
Ok(Self(s))
}
}
impl<'a> TryFrom<&'a str> for &'a UsernameRef {
type Error = Error;
fn try_from(s: &'a str) -> Result<&'a UsernameRef, Error> {
if !PROXMOX_USER_NAME_REGEX.is_match(s) {
bail!("invalid name in user id");
}
Ok(UsernameRef::new(s))
}
}
#[api(schema: PROXMOX_AUTH_REALM_SCHEMA)]
/// An authentication realm.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Realm(String);
/// A reference to an authentication realm.
///
/// This is like a `str` to the `String` of a `Realm`.
#[derive(Debug, Hash, Eq, PartialEq)]
pub struct RealmRef(str);
impl RealmRef {
fn new(s: &str) -> &Self {
unsafe { &*(s as *const str as *const RealmRef) }
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for Realm {
type Target = RealmRef;
fn deref(&self) -> &RealmRef {
self.borrow()
}
}
impl Borrow<RealmRef> for Realm {
fn borrow(&self) -> &RealmRef {
RealmRef::new(self.0.as_str())
}
}
impl AsRef<RealmRef> for Realm {
fn as_ref(&self) -> &RealmRef {
self.borrow()
}
}
impl ToOwned for RealmRef {
type Owned = Realm;
fn to_owned(&self) -> Self::Owned {
Realm(self.0.to_owned())
}
}
impl TryFrom<String> for Realm {
type Error = Error;
fn try_from(s: String) -> Result<Self, Error> {
PROXMOX_AUTH_REALM_STRING_SCHEMA
.check_constraints(&s)
.map_err(|_| format_err!("invalid realm"))?;
Ok(Self(s))
}
}
impl<'a> TryFrom<&'a str> for &'a RealmRef {
type Error = Error;
fn try_from(s: &'a str) -> Result<&'a RealmRef, Error> {
PROXMOX_AUTH_REALM_STRING_SCHEMA
.check_constraints(s)
.map_err(|_| format_err!("invalid realm"))?;
Ok(RealmRef::new(s))
}
}
impl PartialEq<str> for Realm {
fn eq(&self, rhs: &str) -> bool {
self.0 == rhs
}
}
impl PartialEq<&str> for Realm {
fn eq(&self, rhs: &&str) -> bool {
self.0 == *rhs
}
}
impl PartialEq<str> for RealmRef {
fn eq(&self, rhs: &str) -> bool {
self.0 == *rhs
}
}
impl PartialEq<&str> for RealmRef {
fn eq(&self, rhs: &&str) -> bool {
self.0 == **rhs
}
}
impl PartialEq<RealmRef> for Realm {
fn eq(&self, rhs: &RealmRef) -> bool {
self.0 == rhs.0
}
}
impl PartialEq<Realm> for RealmRef {
fn eq(&self, rhs: &Realm) -> bool {
self.0 == rhs.0
}
}
impl PartialEq<Realm> for &RealmRef {
fn eq(&self, rhs: &Realm) -> bool {
self.0 == rhs.0
}
}
#[api(
type: String,
format: &PROXMOX_TOKEN_NAME_FORMAT,
)]
/// The token ID part of an API token authentication id.
///
/// This alone does NOT uniquely identify the API token - use a full `Authid` for such use cases.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
pub struct Tokenname(String);
/// A reference to a token name part of an authentication id. This alone does NOT uniquely identify
/// the user.
///
/// This is like a `str` to the `String` of a [`Tokenname`].
#[derive(Debug, Hash)]
pub struct TokennameRef(str);
#[doc(hidden)]
/// ```compile_fail
/// let a: Username = unsafe { std::mem::zeroed() };
/// let b: Username = unsafe { std::mem::zeroed() };
/// let _ = <Username as PartialEq>::eq(&a, &b);
/// ```
///
/// ```compile_fail
/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
/// let _ = <&UsernameRef as PartialEq>::eq(a, b);
/// ```
///
/// ```compile_fail
/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
/// let _ = <&UsernameRef as PartialEq>::eq(&a, &b);
/// ```
struct _AssertNoEqImpl;
impl TokennameRef {
fn new(s: &str) -> &Self {
unsafe { &*(s as *const str as *const TokennameRef) }
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for Tokenname {
type Target = TokennameRef;
fn deref(&self) -> &TokennameRef {
self.borrow()
}
}
impl Borrow<TokennameRef> for Tokenname {
fn borrow(&self) -> &TokennameRef {
TokennameRef::new(self.0.as_str())
}
}
impl AsRef<TokennameRef> for Tokenname {
fn as_ref(&self) -> &TokennameRef {
self.borrow()
}
}
impl ToOwned for TokennameRef {
type Owned = Tokenname;
fn to_owned(&self) -> Self::Owned {
Tokenname(self.0.to_owned())
}
}
impl TryFrom<String> for Tokenname {
type Error = Error;
fn try_from(s: String) -> Result<Self, Error> {
if !PROXMOX_TOKEN_NAME_REGEX.is_match(&s) {
bail!("invalid token name");
}
Ok(Self(s))
}
}
impl<'a> TryFrom<&'a str> for &'a TokennameRef {
type Error = Error;
fn try_from(s: &'a str) -> Result<&'a TokennameRef, Error> {
if !PROXMOX_TOKEN_NAME_REGEX.is_match(s) {
bail!("invalid token name in user id");
}
Ok(TokennameRef::new(s))
}
}
/// A complete user id consisting of a user name and a realm
#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, UpdaterType)]
pub struct Userid {
data: String,
name_len: usize,
}
impl ApiType for Userid {
const API_SCHEMA: Schema = StringSchema::new("User ID")
.format(&PROXMOX_USER_ID_FORMAT)
.min_length(3)
.max_length(64)
.schema();
}
impl Userid {
const fn new(data: String, name_len: usize) -> Self {
Self { data, name_len }
}
pub fn name(&self) -> &UsernameRef {
UsernameRef::new(&self.data[..self.name_len])
}
pub fn realm(&self) -> &RealmRef {
RealmRef::new(&self.data[(self.name_len + 1)..])
}
pub fn as_str(&self) -> &str {
&self.data
}
/// Get the "root@pam" user id.
pub fn root_userid() -> &'static Self {
&ROOT_USERID
}
}
lazy_static! {
pub static ref ROOT_USERID: Userid = Userid::new("root@pam".to_string(), 4);
}
impl From<Authid> for Userid {
fn from(authid: Authid) -> Self {
authid.user
}
}
impl From<(Username, Realm)> for Userid {
fn from(parts: (Username, Realm)) -> Self {
Self::from((parts.0.as_ref(), parts.1.as_ref()))
}
}
impl From<(&UsernameRef, &RealmRef)> for Userid {
fn from(parts: (&UsernameRef, &RealmRef)) -> Self {
let data = format!("{}@{}", parts.0.as_str(), parts.1.as_str());
let name_len = parts.0.as_str().len();
Self { data, name_len }
}
}
impl fmt::Display for Userid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.data.fmt(f)
}
}
impl std::str::FromStr for Userid {
type Err = Error;
fn from_str(id: &str) -> Result<Self, Error> {
let name_len = id
.as_bytes()
.iter()
.rposition(|&b| b == b'@')
.ok_or_else(|| format_err!("not a valid user id"))?;
let name = &id[..name_len];
let realm = &id[(name_len + 1)..];
if !PROXMOX_USER_NAME_REGEX.is_match(name) {
bail!("invalid user name in user id");
}
PROXMOX_AUTH_REALM_STRING_SCHEMA
.check_constraints(realm)
.map_err(|_| format_err!("invalid realm in user id"))?;
Ok(Self::from((UsernameRef::new(name), RealmRef::new(realm))))
}
}
impl TryFrom<String> for Userid {
type Error = Error;
fn try_from(data: String) -> Result<Self, Error> {
let name_len = data
.as_bytes()
.iter()
.rposition(|&b| b == b'@')
.ok_or_else(|| format_err!("not a valid user id"))?;
if !PROXMOX_USER_NAME_REGEX.is_match(&data[..name_len]) {
bail!("invalid user name in user id");
}
PROXMOX_AUTH_REALM_STRING_SCHEMA
.check_constraints(&data[(name_len + 1)..])
.map_err(|_| format_err!("invalid realm in user id"))?;
Ok(Self { data, name_len })
}
}
impl PartialEq<str> for Userid {
fn eq(&self, rhs: &str) -> bool {
self.data == *rhs
}
}
impl PartialEq<&str> for Userid {
fn eq(&self, rhs: &&str) -> bool {
*self == **rhs
}
}
impl PartialEq<String> for Userid {
fn eq(&self, rhs: &String) -> bool {
self == rhs.as_str()
}
}
/// A complete authentication id consisting of a user id and an optional token name.
#[derive(Clone, Debug, Eq, PartialEq, Hash, UpdaterType, Ord, PartialOrd)]
pub struct Authid {
user: Userid,
tokenname: Option<Tokenname>,
}
impl ApiType for Authid {
const API_SCHEMA: Schema = StringSchema::new("Authentication ID")
.format(&PROXMOX_AUTH_ID_FORMAT)
.min_length(3)
.max_length(64)
.schema();
}
impl Authid {
const fn new(user: Userid, tokenname: Option<Tokenname>) -> Self {
Self { user, tokenname }
}
pub fn user(&self) -> &Userid {
&self.user
}
pub fn is_token(&self) -> bool {
self.tokenname.is_some()
}
pub fn tokenname(&self) -> Option<&TokennameRef> {
self.tokenname.as_deref()
}
/// Get the "root@pam" auth id.
pub fn root_auth_id() -> &'static Self {
&ROOT_AUTHID
}
}
lazy_static! {
pub static ref ROOT_AUTHID: Authid = Authid::from(Userid::new("root@pam".to_string(), 4));
}
impl From<Userid> for Authid {
fn from(parts: Userid) -> Self {
Self::new(parts, None)
}
}
impl From<(Userid, Option<Tokenname>)> for Authid {
fn from(parts: (Userid, Option<Tokenname>)) -> Self {
Self::new(parts.0, parts.1)
}
}
impl fmt::Display for Authid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.tokenname {
Some(token) => write!(f, "{}!{}", self.user, token.as_str()),
None => self.user.fmt(f),
}
}
}
impl std::str::FromStr for Authid {
type Err = Error;
fn from_str(id: &str) -> Result<Self, Error> {
let name_len = id
.as_bytes()
.iter()
.rposition(|&b| b == b'@')
.ok_or_else(|| format_err!("not a valid user id"))?;
let realm_end = id
.as_bytes()
.iter()
.rposition(|&b| b == b'!')
.map(|pos| if pos < name_len { id.len() } else { pos })
.unwrap_or_else(|| id.len());
if realm_end == id.len() - 1 {
bail!("empty token name in userid");
}
let user = Userid::from_str(&id[..realm_end])?;
if id.len() > realm_end {
let token = Tokenname::try_from(id[(realm_end + 1)..].to_string())?;
Ok(Self::new(user, Some(token)))
} else {
Ok(Self::new(user, None))
}
}
}
impl TryFrom<String> for Authid {
type Error = Error;
fn try_from(mut data: String) -> Result<Self, Error> {
let name_len = data
.as_bytes()
.iter()
.rposition(|&b| b == b'@')
.ok_or_else(|| format_err!("not a valid user id"))?;
let realm_end = data
.as_bytes()
.iter()
.rposition(|&b| b == b'!')
.map(|pos| if pos < name_len { data.len() } else { pos })
.unwrap_or_else(|| data.len());
if realm_end == data.len() - 1 {
bail!("empty token name in userid");
}
let tokenname = if data.len() > realm_end {
Some(Tokenname::try_from(data[(realm_end + 1)..].to_string())?)
} else {
None
};
data.truncate(realm_end);
let user: Userid = data.parse()?;
Ok(Self { user, tokenname })
}
}
#[test]
fn test_token_id() {
let userid: Userid = "test@pam".parse().expect("parsing Userid failed");
assert_eq!(userid.name().as_str(), "test");
assert_eq!(userid.realm(), "pam");
assert_eq!(userid, "test@pam");
let auth_id: Authid = "test@pam".parse().expect("parsing user Authid failed");
assert_eq!(auth_id.to_string(), "test@pam".to_string());
assert!(!auth_id.is_token());
assert_eq!(auth_id.user(), &userid);
let user_auth_id = Authid::from(userid.clone());
assert_eq!(user_auth_id, auth_id);
assert!(!user_auth_id.is_token());
let auth_id: Authid = "test@pam!bar".parse().expect("parsing token Authid failed");
let token_userid = auth_id.user();
assert_eq!(&userid, token_userid);
assert!(auth_id.is_token());
assert_eq!(
auth_id.tokenname().expect("Token has tokenname").as_str(),
TokennameRef::new("bar").as_str()
);
assert_eq!(auth_id.to_string(), "test@pam!bar".to_string());
}
serde_plain::derive_deserialize_from_fromstr!(Userid, "valid user id");
serde_plain::derive_serialize_from_display!(Userid);
serde_plain::derive_deserialize_from_fromstr!(Authid, "valid user id or token id");
serde_plain::derive_serialize_from_display!(Authid);

View File

@ -308,7 +308,7 @@ fn need_description(description: Option<String>) -> Result<String, Error> {
#[allow(clippy::too_many_arguments)]
pub fn add_tfa_entry<A: OpenUserChallengeData>(
config: &mut TfaConfig,
access: A,
access: &A,
userid: &str,
description: Option<String>,
totp: Option<String>,
@ -410,9 +410,9 @@ fn add_yubico(
)))
}
fn add_u2f<A: OpenUserChallengeData>(
fn add_u2f<A: ?Sized + OpenUserChallengeData>(
config: &mut TfaConfig,
access: A,
access: &A,
userid: &str,
description: Option<String>,
challenge: Option<String>,
@ -436,9 +436,9 @@ fn add_u2f<A: OpenUserChallengeData>(
}
}
fn add_webauthn<A: OpenUserChallengeData>(
fn add_webauthn<A: ?Sized + OpenUserChallengeData>(
config: &mut TfaConfig,
access: A,
access: &A,
userid: &str,
description: Option<String>,
challenge: Option<String>,

View File

@ -43,22 +43,18 @@ trait IsExpired {
fn is_expired(&self, at_epoch: i64) -> bool;
}
pub trait OpenUserChallengeData: Clone {
type Data: UserChallengeAccess;
pub trait OpenUserChallengeData {
fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error>;
fn open(&self, userid: &str) -> Result<Self::Data, Error>;
fn open_no_create(&self, userid: &str) -> Result<Option<Self::Data>, Error>;
fn open_no_create(&self, userid: &str) -> Result<Option<Box<dyn UserChallengeAccess>>, Error>;
/// Should return `true` if something was removed, `false` if no data existed for the user.
fn remove(&self, userid: &str) -> Result<bool, Error>;
}
pub trait UserChallengeAccess: Sized {
//fn open(userid: &str) -> Result<Self, Error>;
//fn open_no_create(userid: &str) -> Result<Option<Self>, Error>;
pub trait UserChallengeAccess {
fn get_mut(&mut self) -> &mut TfaUserChallenges;
fn save(self) -> Result<(), Error>;
fn save(&mut self) -> Result<(), Error>;
}
const CHALLENGE_TIMEOUT_SECS: i64 = 2 * 60;
@ -115,9 +111,9 @@ fn check_webauthn<'a, 'config: 'a, 'origin: 'a>(
impl TfaConfig {
// Get a u2f registration challenge.
pub fn u2f_registration_challenge<A: OpenUserChallengeData>(
pub fn u2f_registration_challenge<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
description: String,
) -> Result<String, Error> {
@ -130,9 +126,9 @@ impl TfaConfig {
}
/// Finish a u2f registration challenge.
pub fn u2f_registration_finish<A: OpenUserChallengeData>(
pub fn u2f_registration_finish<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
challenge: &str,
response: &str,
@ -146,9 +142,9 @@ impl TfaConfig {
}
/// Get a webauthn registration challenge.
pub fn webauthn_registration_challenge<A: OpenUserChallengeData>(
pub fn webauthn_registration_challenge<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
user: &str,
description: String,
origin: Option<&Url>,
@ -162,9 +158,9 @@ impl TfaConfig {
}
/// Finish a webauthn registration challenge.
pub fn webauthn_registration_finish<A: OpenUserChallengeData>(
pub fn webauthn_registration_finish<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
challenge: &str,
response: &str,
@ -215,9 +211,9 @@ impl TfaConfig {
}
/// Get a two factor authentication challenge for a user, if the user has TFA set up.
pub fn authentication_challenge<A: OpenUserChallengeData>(
pub fn authentication_challenge<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
origin: Option<&Url>,
) -> Result<Option<TfaChallenge>, Error> {
@ -233,9 +229,9 @@ impl TfaConfig {
}
/// Verify a TFA challenge.
pub fn verify<A: OpenUserChallengeData>(
pub fn verify<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
challenge: &TfaChallenge,
response: TfaResponse,
@ -266,9 +262,9 @@ impl TfaConfig {
Ok(NeedsSaving::No)
}
pub fn remove_user<A: OpenUserChallengeData>(
pub fn remove_user<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
) -> Result<NeedsSaving, Error> {
let mut save = access.remove(userid)?;
@ -388,9 +384,9 @@ impl TfaUserData {
/// challenges in the tfa config file if necessary. The user otherwise has no access to this
/// information at this point, as the challenge is identified by its actual challenge data
/// instead.
fn u2f_registration_challenge<A: OpenUserChallengeData>(
fn u2f_registration_challenge<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
u2f: &u2f::U2f,
description: String,
@ -409,9 +405,9 @@ impl TfaUserData {
Ok(challenge)
}
fn u2f_registration_finish<A: OpenUserChallengeData>(
fn u2f_registration_finish<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
u2f: &u2f::U2f,
challenge: &str,
@ -434,9 +430,9 @@ impl TfaUserData {
/// challenges in the tfa config file if necessary. The user otherwise has no access to this
/// information at this point, as the challenge is identified by its actual challenge data
/// instead.
fn webauthn_registration_challenge<A: OpenUserChallengeData>(
fn webauthn_registration_challenge<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
webauthn: Webauthn<WebauthnConfigInstance>,
userid: &str,
description: String,
@ -473,9 +469,9 @@ impl TfaUserData {
/// Finish a webauthn registration. The challenge should correspond to an output of
/// `webauthn_registration_challenge`. The response should come directly from the client.
fn webauthn_registration_finish<A: OpenUserChallengeData>(
fn webauthn_registration_finish<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
webauthn: Webauthn<WebauthnConfigInstance>,
userid: &str,
challenge: &str,
@ -568,9 +564,9 @@ impl TfaUserData {
}
/// Generate a generic TFA challenge. See the [`TfaChallenge`] description for details.
fn challenge<A: OpenUserChallengeData>(
fn challenge<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
webauthn: Option<Result<Webauthn<WebauthnConfigInstance>, Error>>,
u2f: Option<&u2f::U2f>,
@ -583,7 +579,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, userid, webauthn?)?,
None => None,
},
u2f: match u2f {
@ -600,9 +596,9 @@ impl TfaUserData {
}
/// Generate an optional webauthn challenge.
fn webauthn_challenge<A: OpenUserChallengeData>(
fn webauthn_challenge<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
webauthn: Webauthn<WebauthnConfigInstance>,
) -> Result<Option<webauthn_rs::proto::RequestChallengeResponse>, Error> {
@ -632,9 +628,9 @@ impl TfaUserData {
}
/// Generate an optional u2f challenge.
fn u2f_challenge<A: OpenUserChallengeData>(
fn u2f_challenge<A: ?Sized + OpenUserChallengeData>(
&self,
access: A,
access: &A,
userid: &str,
u2f: &u2f::U2f,
) -> Result<Option<U2fChallenge>, Error> {
@ -666,9 +662,9 @@ impl TfaUserData {
}
/// Verify a u2f response.
fn verify_u2f<A: OpenUserChallengeData>(
fn verify_u2f<A: ?Sized + OpenUserChallengeData>(
&self,
access: A,
access: &A,
userid: &str,
u2f: u2f::U2f,
challenge: &crate::u2f::AuthChallenge,
@ -712,9 +708,9 @@ impl TfaUserData {
}
/// Verify a webauthn response.
fn verify_webauthn<A: OpenUserChallengeData>(
fn verify_webauthn<A: ?Sized + OpenUserChallengeData>(
&mut self,
access: A,
access: &A,
userid: &str,
webauthn: Webauthn<WebauthnConfigInstance>,
mut response: Value,