my $pve_auth_key_cache = {};
-my $ticket_lifetime = 3600*2; # 2 hours
-# TODO: set to 24h for PVE 6.0
-my $authkey_lifetime = 3600*0; # rotation disabled
+my $ticket_lifetime = 3600 * 2; # 2 hours
+my $authkey_lifetime = 3600 * 24; # rotate every 24 hours
Crypt::OpenSSL::RSA->import_random_seed();
return if check_authkey();
my $old = get_pubkey();
+ my $new = Crypt::OpenSSL::RSA->generate_key(2048);
if ($old) {
eval {
my $pem = $old->get_public_key_x509_string();
+ # mtime is used for caching and ticket age range calculation
PVE::Tools::file_set_contents($pve_auth_key_files->{pubold}, $pem);
};
die "Failed to store old auth key: $@\n" if $@;
}
- my $new = Crypt::OpenSSL::RSA->generate_key(2048);
eval {
my $pem = $new->get_public_key_x509_string();
+ # mtime is used for caching and ticket age range calculation,
+ # should be close to that of pubold above
PVE::Tools::file_set_contents($pve_auth_key_files->{pub}, $pem);
};
if ($@) {
}
my $csrf_prevention_secret;
+my $csrf_prevention_secret_legacy;
my $get_csrfr_secret = sub {
if (!$csrf_prevention_secret) {
my $input = PVE::Tools::file_get_contents($pve_www_key_fn);
- $csrf_prevention_secret = Digest::SHA::sha1_base64($input);
+ $csrf_prevention_secret = Digest::SHA::hmac_sha256_base64($input);
+ $csrf_prevention_secret_legacy = Digest::SHA::sha1_base64($input);
}
return $csrf_prevention_secret;
};
sub verify_csrf_prevention_token {
my ($username, $token, $noerr) = @_;
- my $secret = &$get_csrfr_secret();
+ my $secret = $get_csrfr_secret->();
+
+ # FIXME: remove with PVE 7 and/or refactor all into PVE::Ticket ?
+ if ($token =~ m/^([A-Z0-9]{8}):(\S+)$/) {
+ my $sig = $2;
+ if (length($sig) == 27) {
+ # the legacy secret got populated by above get_csrfr_secret call
+ $secret = $csrf_prevention_secret_legacy;
+ }
+ }
return PVE::Ticket::verify_csrf_prevention_token(
$secret, $username, $token, -300, $ticket_lifetime, $noerr);
return undef if !$rsa_pub;
my ($min, $max) = $get_ticket_age_range->($now, $rsa_mtime, $old);
- return undef if !$min;
+ return undef if !defined($min);
return PVE::Ticket::verify_rsa_ticket(
$rsa_pub, 'PVE', $ticket, undef, $min, $max, 1);
return $auth_failure->();
}
- my ($username, $challenge);
+ my ($username, $tfa_info);
if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
# Ticket for u2f-users:
- ($username, $challenge) = ($1, $2);
+ ($username, my $challenge) = ($1, $2);
if ($challenge eq 'verified') {
# u2f challenge was completed
$challenge = undef;
# so we treat this ticket as invalid:
return $auth_failure->();
}
+ $tfa_info = {
+ type => 'u2f',
+ challenge => $challenge,
+ };
+ } elsif ($data =~ /^tfa!(.*)$/) {
+ # TOTP and Yubico don't require a challenge so this is the generic
+ # 'missing 2nd factor ticket'
+ $username = $1;
+ $tfa_info = { type => 'tfa' };
} else {
# Regular ticket (full access)
$username = $data;
return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
- return wantarray ? ($username, $age, $challenge) : $username;
+ return wantarray ? ($username, $age, $tfa_info) : $username;
}
# VNC tickets
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
$plugin->authenticate_user($cfg, $realm, $ruid, $password);
- my $u2f;
-
my ($type, $tfa_data) = user_get_tfa($username, $realm);
if ($type) {
if ($type eq 'u2f') {
# Note that if the user did not manage to complete the initial u2f registration
# challenge we have a hash containing a 'challenge' entry in the user's tfa.cfg entry:
- $u2f = $tfa_data if !exists $tfa_data->{challenge};
+ $tfa_data = undef if exists $tfa_data->{challenge};
+ } elsif (!defined($otp)) {
+ # The user requires a 2nd factor but has not provided one. Return success but
+ # don't clear $tfa_data.
} else {
my $keys = $tfa_data->{keys};
my $tfa_cfg = $tfa_data->{config};
verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
+ $tfa_data = undef;
+ }
+
+ # Return the type along with the rest:
+ if ($tfa_data) {
+ $tfa_data = {
+ type => $type,
+ data => $tfa_data,
+ };
}
}
- return wantarray ? ($username, $u2f) : $username;
+ return wantarray ? ($username, $tfa_data) : $username;
}
sub domain_set_password {
}
foreach my $ug (split_list($uglist)) {
- if ($ug =~ m/^@(\S+)$/) {
- my $group = $1;
+ my ($group) = $ug =~ m/^@(\S+)$/;
+
+ if ($group && verify_groupname($group, 1)) {
if ($cfg->{groups}->{$group}) { # group exists
$cfg->{acl}->{$path}->{groups}->{$group}->{$role} = $propagate;
} else {
my $data = '';
- foreach my $user (keys %{$cfg->{users}}) {
+ foreach my $user (sort keys %{$cfg->{users}}) {
my $d = $cfg->{users}->{$user};
my $firstname = $d->{firstname} ? PVE::Tools::encode_text($d->{firstname}) : '';
my $lastname = $d->{lastname} ? PVE::Tools::encode_text($d->{lastname}) : '';
$data .= "\n";
- foreach my $group (keys %{$cfg->{groups}}) {
+ foreach my $group (sort keys %{$cfg->{groups}}) {
my $d = $cfg->{groups}->{$group};
my $list = join (',', keys %{$d->{users}});
my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : '';
$data .= "\n";
- foreach my $pool (keys %{$cfg->{pools}}) {
+ foreach my $pool (sort keys %{$cfg->{pools}}) {
my $d = $cfg->{pools}->{$pool};
my $vmlist = join (',', keys %{$d->{vms}});
my $storelist = join (',', keys %{$d->{storage}});
$data .= "\n";
- foreach my $role (keys %{$cfg->{roles}}) {
+ foreach my $role (sort keys %{$cfg->{roles}}) {
next if $special_roles->{$role};
my $d = $cfg->{roles}->{$role};
lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed");
}
-my $CUSTOM_TFA_TYPES = {
+my $USER_CONTROLLED_TFA_TYPES = {
u2f => 1,
oath => 1,
};
# The 'yubico' type requires yubico server settings, which have to be configured on the
# realm, so this is not supported here:
die "domain '$realm' does not support TFA type '$type'\n"
- if defined($data) && !$CUSTOM_TFA_TYPES->{$type};
+ if defined($data) && !$USER_CONTROLLED_TFA_TYPES->{$type};
}
# Custom TFA entries are stored in priv/tfa.cfg as they can be more complet: u2f uses a
$tfa->{data} = $data;
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
- $user->{keys} = 'x';
+ $user->{keys} = "x!$type";
} else {
delete $tfa_cfg->{users}->{$userid};
cfs_write_file('priv/tfa.cfg', $tfa_cfg);
or die "user '$username' not found\n";
my $keys = $user->{keys};
- return if !$keys;
my $domain_cfg = cfs_read_file('domains.cfg');
my $realm_cfg = $domain_cfg->{ids}->{$realm};
$realm_tfa = PVE::Auth::Plugin::parse_tfa_config($realm_tfa)
if $realm_tfa;
- if ($keys ne 'x') {
+ if (!$keys) {
+ return if !$realm_tfa;
+ die "missing required 2nd keys\n";
+ }
+
+ # new style config starts with an 'x' and optionally contains a !<type> suffix
+ if ($keys !~ /^x(?:!.*)?$/) {
# old style config, find the type via the realm
return if !$realm_tfa;
return ($realm_tfa->{type}, {