all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH access-control 03/10] use PBS-like auth api call flow
Date: Tue,  9 Nov 2021 12:26:58 +0100	[thread overview]
Message-ID: <20211109112721.130935-10-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20211109112721.130935-1-w.bumiller@proxmox.com>

The main difference here is that we have no separate api
path for verification. Instead, the ticket api call gets an
optional 'tfa-challenge' parameter.

This is opt-in: old pve-manager UI with new
pve-access-control will still work as expected, but users
won't be able to *update* their TFA settings.

Since the new tfa config parser will build a compatible
in-perl tfa config object as well, the old authentication
code is left unchanged for compatibility and will be removed
with pve-8, where the `new-format` parameter in the ticket
call will change its default to `1`.

The `change_tfa` call will simply die in this commit. It is
removed later when adding the pbs-style TFA API calls.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
 src/PVE/API2/AccessControl.pm |  79 ++++++++++++++----
 src/PVE/AccessControl.pm      | 153 +++++++++++++++++++++++++++++-----
 2 files changed, 199 insertions(+), 33 deletions(-)

diff --git a/src/PVE/API2/AccessControl.pm b/src/PVE/API2/AccessControl.pm
index 6dec66c..8fa3606 100644
--- a/src/PVE/API2/AccessControl.pm
+++ b/src/PVE/API2/AccessControl.pm
@@ -105,8 +105,8 @@ __PACKAGE__->register_method ({
     }});
 
 
-my $verify_auth = sub {
-    my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs) = @_;
+my sub verify_auth : prototype($$$$$$$) {
+    my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs, $new_format) = @_;
 
     my $normpath = PVE::AccessControl::normalize_path($path);
 
@@ -117,7 +117,12 @@ my $verify_auth = sub {
     } elsif (PVE::AccessControl::verify_vnc_ticket($pw_or_ticket, $username, $normpath, 1)) {
 	# valid vnc ticket
     } else {
-	$username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
+	$username = PVE::AccessControl::authenticate_user(
+	    $username,
+	    $pw_or_ticket,
+	    $otp,
+	    $new_format,
+	);
     }
 
     my $privlist = [ PVE::Tools::split_list($privs) ];
@@ -128,22 +133,45 @@ my $verify_auth = sub {
     return { username => $username };
 };
 
-my $create_ticket = sub {
-    my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
+my sub create_ticket_do : prototype($$$$$$) {
+    my ($rpcenv, $username, $pw_or_ticket, $otp, $new_format, $tfa_challenge) = @_;
+
+    die "TFA response should be in 'password', not 'otp' when 'tfa-challenge' is set\n"
+	if defined($otp) && defined($tfa_challenge);
+
+    my ($ticketuser, undef, $tfa_info);
+    if (!defined($tfa_challenge)) {
+	# We only verify this ticket if we're not responding to a TFA challenge, as in that case
+	# it is a TFA-data ticket and will be verified by `authenticate_user`.
+
+	($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
+    }
 
-    my ($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
     if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
 	if (defined($tfa_info)) {
 	    die "incomplete ticket\n";
 	}
 	# valid ticket. Note: root@pam can create tickets for other users
     } else {
-	($username, $tfa_info) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
+	($username, $tfa_info) = PVE::AccessControl::authenticate_user(
+	    $username,
+	    $pw_or_ticket,
+	    $otp,
+	    $new_format,
+	    $tfa_challenge,
+	);
     }
 
     my %extra;
     my $ticket_data = $username;
-    if (defined($tfa_info)) {
+    my $aad;
+    if ($new_format) {
+	if (defined($tfa_info)) {
+	    $extra{NeedTFA} = 1;
+	    $ticket_data = "!tfa!$tfa_info";
+	    $aad = $username;
+	}
+    } elsif (defined($tfa_info)) {
 	$extra{NeedTFA} = 1;
 	if ($tfa_info->{type} eq 'u2f') {
 	    my $u2finfo = $tfa_info->{data};
@@ -159,7 +187,7 @@ my $create_ticket = sub {
 	}
     }
 
-    my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
+    my $ticket = PVE::AccessControl::assemble_ticket($ticket_data, $aad);
     my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
 
     return {
@@ -230,6 +258,20 @@ __PACKAGE__->register_method ({
 		optional => 1,
 		maxLength => 64,
 	    },
+	    'new-format' => {
+		type => 'boolean',
+		description =>
+		    'With webauthn the format of half-authenticated tickts changed.'
+		    .' New clients should pass 1 here and not worry about the old format.'
+		    .' The old format is deprecated and will be retired with PVE-8.0',
+		optional => 1,
+		default => 0,
+	    },
+	    'tfa-challenge' => {
+		type => 'string',
+                description => "The signed TFA challenge string the user wants to respond to.",
+		optional => 1,
+	    },
 	}
     },
     returns => {
@@ -257,10 +299,17 @@ __PACKAGE__->register_method ({
 	    $rpcenv->check_user_enabled($username);
 
 	    if ($param->{path} && $param->{privs}) {
-		$res = &$verify_auth($rpcenv, $username, $param->{password}, $param->{otp},
-				     $param->{path}, $param->{privs});
+		$res = verify_auth($rpcenv, $username, $param->{password}, $param->{otp},
+				   $param->{path}, $param->{privs}, $param->{'new-format'});
 	    } else {
-		$res = &$create_ticket($rpcenv, $username, $param->{password}, $param->{otp});
+		$res = create_ticket_do(
+		    $rpcenv,
+		    $username,
+		    $param->{password},
+		    $param->{otp},
+		    $param->{'new-format'},
+		    $param->{'tfa-challenge'},
+		);
 	    }
 	};
 	if (my $err = $@) {
@@ -476,6 +525,8 @@ __PACKAGE__->register_method ({
     code => sub {
 	my ($param) = @_;
 
+	die "TODO!\n";
+
 	my $rpcenv = PVE::RPCEnvironment::get();
 	my $authuser = $rpcenv->get_user();
 
@@ -528,7 +579,7 @@ __PACKAGE__->register_method ({
 	    raise_param_exc({ 'response' => "confirm action requires the 'response' parameter to be set" })
 		if !defined($response);
 
-	    my ($type, $u2fdata) = PVE::AccessControl::user_get_tfa($userid, $realm);
+	    my ($type, $u2fdata) = PVE::AccessControl::user_get_tfa($userid, $realm, 'FIXME');
 	    raise("no u2f data available")
 		if (!defined($type) || $type ne 'u2f');
 
@@ -580,7 +631,7 @@ __PACKAGE__->register_method({
 	my $authuser = $rpcenv->get_user();
 	my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
 
-	my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
+	my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm, 0);
 	if (!defined($tfa_type)) {
 	    raise('no u2f data available');
 	}
diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
index 2fa2695..d61d7f4 100644
--- a/src/PVE/AccessControl.pm
+++ b/src/PVE/AccessControl.pm
@@ -338,16 +338,23 @@ my $get_ticket_age_range = sub {
     return ($min, $max);
 };
 
-sub assemble_ticket {
-    my ($data) = @_;
+sub assemble_ticket : prototype($;$) {
+    my ($data, $aad) = @_;
 
     my $rsa_priv = get_privkey();
 
-    return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PVE', $data);
+    return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PVE', $data, $aad);
 }
 
-sub verify_ticket {
-    my ($ticket, $noerr) = @_;
+# Returns the username, "age" and tfa info.
+#
+# Note that for the new-style outh, tfa info is never set, as it only uses the `/ticket` api call
+# via the new 'tfa-challenge' parameter, so this part can go with PVE-8.
+#
+# New-style auth still uses this function, but sets `$tfa_ticket` to true when validating the tfa
+# ticket.
+sub verify_ticket : prototype($;$$) {
+    my ($ticket, $noerr, $tfa_ticket_aad) = @_;
 
     my $now = time();
 
@@ -361,7 +368,7 @@ sub verify_ticket {
 	return undef if !defined($min);
 
 	return PVE::Ticket::verify_rsa_ticket(
-	    $rsa_pub, 'PVE', $ticket, undef, $min, $max, 1);
+	    $rsa_pub, 'PVE', $ticket, $tfa_ticket_aad, $min, $max, 1);
     };
 
     my ($data, $age) = $check->();
@@ -382,7 +389,21 @@ sub verify_ticket {
 	return $auth_failure->();
     }
 
+    if ($tfa_ticket_aad) {
+	# We're validating a ticket-call's 'tfa-challenge' parameter, so just return its data.
+	if ($data =~ /^!tfa!(.*)$/) {
+	    return $1;
+	}
+	die "bad ticket\n";
+    }
+
     my ($username, $tfa_info);
+    if ($data =~ /^!tfa!(.*)$/) {
+	# PBS style half-authenticated ticket, contains a json string form of a `TfaChallenge`
+	# object.
+	# This type of ticket does not contain the user name.
+	return { type => 'new', data => $1 };
+    }
     if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
 	# Ticket for u2f-users:
 	($username, my $challenge) = ($1, $2);
@@ -623,8 +644,8 @@ sub verify_one_time_pw {
 
 # password should be utf8 encoded
 # Note: some plugins delay/sleep if auth fails
-sub authenticate_user {
-    my ($username, $password, $otp) = @_;
+sub authenticate_user : prototype($$$$;$) {
+    my ($username, $password, $otp, $new_format, $tfa_challenge) = @_;
 
     die "no username specified\n" if !$username;
 
@@ -641,9 +662,28 @@ sub authenticate_user {
     my $cfg = $domain_cfg->{ids}->{$realm};
     die "auth domain '$realm' does not exist\n" if !$cfg;
     my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+
+    if ($tfa_challenge) {
+	# This is the 2nd factor, use the password for the OTP response.
+	my $tfa_challenge = authenticate_2nd_new($username, $realm, $password, $tfa_challenge);
+	return wantarray ? ($username, $tfa_challenge) : $username;
+    }
+
     $plugin->authenticate_user($cfg, $realm, $ruid, $password);
 
-    my ($type, $tfa_data) = user_get_tfa($username, $realm);
+    if ($new_format) {
+	# This is the first factor with an optional immediate 2nd factor for TOTP:
+	my $tfa_challenge = authenticate_2nd_new($username, $realm, $otp, $tfa_challenge);
+	return wantarray ? ($username, $tfa_challenge) : $username;
+    } else {
+	return authenticate_2nd_old($username, $realm, $otp);
+    }
+}
+
+sub authenticate_2nd_old : prototype($$$) {
+    my ($username, $realm, $otp) = @_;
+
+    my ($type, $tfa_data) = user_get_tfa($username, $realm, 0);
     if ($type) {
 	if ($type eq 'u2f') {
 	    # Note that if the user did not manage to complete the initial u2f registration
@@ -671,6 +711,77 @@ sub authenticate_user {
     return wantarray ? ($username, $tfa_data) : $username;
 }
 
+# Returns a tfa challenge or undef.
+sub authenticate_2nd_new : prototype($$$$) {
+    my ($username, $realm, $otp, $tfa_challenge) = @_;
+
+    return lock_tfa_config(sub {
+	my ($tfa_cfg, $realm_tfa) = user_get_tfa($username, $realm, 1);
+
+	if (!defined($tfa_cfg)) {
+	    return undef;
+	}
+
+	my $realm_type = $realm_tfa && $realm_tfa->{type};
+	if (defined($realm_type) && $realm_type eq 'yubico') {
+	    $tfa_cfg->set_yubico_config({
+		id => $realm_tfa->{id},
+		key => $realm_tfa->{key},
+		url => $realm_tfa->{url},
+	    });
+	}
+
+	configure_u2f_and_wa($tfa_cfg);
+
+	my $must_save = 0;
+	if (defined($tfa_challenge)) {
+	    $tfa_challenge = verify_ticket($tfa_challenge, 0, $username);
+	    $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
+	    $tfa_challenge = undef;
+	} else {
+	    $tfa_challenge = $tfa_cfg->authentication_challenge($username);
+	    if (defined($otp)) {
+		if (defined($tfa_challenge)) {
+		    $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
+		} else {
+		    die "no such challenge\n";
+		}
+	    }
+	}
+
+	if ($must_save) {
+	    cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+	}
+
+	return $tfa_challenge;
+    });
+}
+
+sub configure_u2f_and_wa : prototype($) {
+    my ($tfa_cfg) = @_;
+
+    my $dc = cfs_read_file('datacenter.cfg');
+    if (my $u2f = $dc->{u2f}) {
+	my $origin = $u2f->{origin};
+	if (!defined($origin)) {
+	    my $rpcenv = PVE::RPCEnvironment::get();
+	    $origin = $rpcenv->get_request_host(1);
+	    if ($origin) {
+		$origin = "https://$origin";
+	    } else {
+		die "failed to figure out u2f origin\n";
+	    }
+	}
+	$tfa_cfg->set_u2f_config({
+	    origin => $origin,
+	    appid => $u2f->{appid},
+	});
+    }
+    if (my $wa = $dc->{webauthn}) {
+	$tfa_cfg->set_webauthn_config($wa);
+    }
+}
+
 sub domain_set_password {
     my ($realm, $username, $password) = @_;
 
@@ -1630,8 +1741,8 @@ sub user_set_tfa {
     cfs_write_file('user.cfg', $user_cfg) if defined($user);
 }
 
-sub user_get_tfa {
-    my ($username, $realm) = @_;
+sub user_get_tfa : prototype($$$) {
+    my ($username, $realm, $new_format) = @_;
 
     my $user_cfg = cfs_read_file('user.cfg');
     my $user = $user_cfg->{users}->{$username}
@@ -1662,16 +1773,20 @@ sub user_get_tfa {
 	});
     } else {
 	my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
-	my $tfa = $tfa_cfg->{users}->{$username};
-	return if !$tfa; # should not happen (user.cfg wasn't cleaned up?)
+	if ($new_format) {
+	    return ($tfa_cfg, $realm_tfa);
+	} else {
+	    my $tfa = $tfa_cfg->{users}->{$username};
+	    return if !$tfa; # should not happen (user.cfg wasn't cleaned up?)
 
-	if ($realm_tfa) {
-	    # if the realm has a tfa setting we need to verify the type:
-	    die "auth domain '$realm' and user have mismatching TFA settings\n"
-		if $realm_tfa && $realm_tfa->{type} ne $tfa->{type};
-	}
+	    if ($realm_tfa) {
+		# if the realm has a tfa setting we need to verify the type:
+		die "auth domain '$realm' and user have mismatching TFA settings\n"
+		    if $realm_tfa && $realm_tfa->{type} ne $tfa->{type};
+	    }
 
-	return ($tfa->{type}, $tfa->{data});
+	    return ($tfa->{type}, $tfa->{data});
+	}
     }
 }
 
-- 
2.30.2





  parent reply	other threads:[~2021-11-09 11:28 UTC|newest]

Thread overview: 43+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-09 11:26 [pve-devel] [PATCH multiple 0/9] PBS-like TFA support in PVE Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 1/6] import basic skeleton Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 2/6] import pve-rs Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 3/6] move apt to /perl-apt, use PERLMOD_PRODUCT env var Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 4/6] pve: add tfa api Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 5/6] build fix: pmg-rs is not here yet Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 6/6] Add some dev tips to a README Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 01/10] use rust parser for TFA config Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 02/10] update read_user_tfa_type call Wolfgang Bumiller
2021-11-09 11:26 ` Wolfgang Bumiller [this message]
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 04/10] handle yubico authentication in new path Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 05/10] move TFA api path into its own module Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 06/10] add pbs-style TFA API implementation Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 07/10] support registering yubico otp keys Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 08/10] update tfa cleanup when deleting users Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 09/10] pveum: update tfa delete command Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 10/10] set/remove 'x' for tfa keys in user.cfg in new api Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH cluster] add webauthn configuration to datacenter.cfg Wolfgang Bumiller
2021-11-10 10:12   ` [pve-devel] applied: " Thomas Lamprecht
2021-11-09 11:27 ` [pve-devel] [PATCH common] Ticket: uri-escape colons Wolfgang Bumiller
2021-11-09 12:26   ` [pve-devel] applied: " Thomas Lamprecht
2021-11-09 11:27 ` [pve-devel] [PATCH manager 1/7] www: use render_u2f_error from wtk Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 2/7] www: use UserSelector " Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 3/7] use u2f-api.js and qrcode.min.js " Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 4/7] www: switch to new tfa login format Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 5/7] www: use af-address-book-o for realms Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 6/7] www: add TFA view to config Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 7/7] www: redirect user TFA button to TFA view Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 1/7] add pmxUserSelector Wolfgang Bumiller
2021-11-10  8:29   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 2/7] add Utils used for u2f and webauthn Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 3/7] add u2f-api.js and qrcode.min.js Wolfgang Bumiller
2021-11-10  8:31   ` Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 4/7] add Proxmox.window.TfaLoginWindow Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 5/7] add totp, wa and recovery creation and tfa edit windows Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 6/7] add Proxmox.panel.TfaView Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 7/7] add yubico otp windows & login support Wolfgang Bumiller
2021-11-10  8:30   ` [pve-devel] applied: " Dominik Csapak
2021-11-11 15:52 ` [pve-devel] applied-series: [PATCH multiple 0/9] PBS-like TFA support in PVE 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=20211109112721.130935-10-w.bumiller@proxmox.com \
    --to=w.bumiller@proxmox.com \
    --cc=pve-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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal