X-Git-Url: https://git.proxmox.com/?p=pve-access-control.git;a=blobdiff_plain;f=PVE%2FAccessControl.pm;h=ea4245c2e58e2159e69773b306e315d2a63f2f2b;hp=b42797b40b5bec15a6113a6c2f9afef62b0397af;hb=1f1c4593a19e4eed928f4518279be23a10898eac;hpb=449037034e2fbd5d0894a05f7369bc6bc894caa0 diff --git a/PVE/AccessControl.pm b/PVE/AccessControl.pm index b42797b..ea4245c 100644 --- a/PVE/AccessControl.pm +++ b/PVE/AccessControl.pm @@ -8,8 +8,8 @@ use Crypt::OpenSSL::RSA; use Net::SSLeay; use Net::IP; use MIME::Base64; +use MIME::Base32; #libmime-base32-perl use Digest::SHA; -use Digest::HMAC_SHA1; use URI::Escape; use LWP::UserAgent; use PVE::Tools qw(run_command lock_file file_get_contents split_list safe_print); @@ -228,19 +228,19 @@ sub assemble_spice_ticket { my $randomstr = "PVESPICE:$timestamp:$vmid:$node:" . rand(10); - # this should be uses as one-time password + # this should be used as one-time password # max length is 60 chars (spice limit) # we pass this to qemu set_pasword and limit lifetime there # keep this secret my $ticket = Digest::SHA::sha1_hex($rsa_priv->sign($randomstr)); # Note: spice proxy connects with HTTP, so $proxyticket is exposed to public - # we use a signature/timestamp to make sure nobody can fake such ticket + # we use a signature/timestamp to make sure nobody can fake such a ticket # an attacker can use this $proxyticket, but he will fail because $ticket is # private. - # The proxy need to be able to extract/verify the ticket + # The proxy needs to be able to extract/verify the ticket # Note: data needs to be lower case only, because virt-viewer needs that - # Note: RSA signature are too long (>=256 charaters) and makes problems with remote-viewer + # Note: RSA signature are too long (>=256 charaters) and make problems with remote-viewer my $secret = &$get_csrfr_secret(); my $plain = "pvespiceproxy:$timestamp:$vmid:" . lc($node); @@ -332,7 +332,7 @@ sub remote_viewer_config { 'release-cursor' => "Ctrl+Alt+R", type => 'spice', title => $title, - host => $proxyticket, # this break tls hostname verification, so we need to use 'host-subject' + host => $proxyticket, # this breaks tls hostname verification, so we need to use 'host-subject' proxy => "http://$proxy:3128", 'tls-port' => $port, 'host-subject' => $subject, @@ -375,7 +375,7 @@ sub verify_one_time_pw { my $type = $tfa_cfg->{type}; - die "missing one time password for Factor-two authentication '$type'\n" if !$otp; + die "missing one time password for two-factor authentication '$type'\n" if !$otp; # fixme: proxy support? my $proxy; @@ -392,7 +392,7 @@ sub verify_one_time_pw { } # password should be utf8 encoded -# Note: some pluging delay/sleep if auth fails +# Note: some plugins delay/sleep if auth fails sub authenticate_user { my ($username, $password, $otp) = @_; @@ -434,7 +434,7 @@ sub domain_set_password { my $domain_cfg = cfs_read_file('domains.cfg'); my $cfg = $domain_cfg->{ids}->{$realm}; - die "auth domain '$realm' does not exists\n" if !$cfg; + die "auth domain '$realm' does not exist\n" if !$cfg; my $plugin = PVE::Auth::Plugin->lookup($cfg->{type}); $plugin->store_password($cfg, $realm, $username, $password); } @@ -488,7 +488,7 @@ sub delete_pool_acl { # into 3 groups (per category) # root: only root is allowed to do that # admin: an administrator can to that -# user: a normak user/customer can to that +# user: a normal user/customer can to that my $privgroups = { VM => { root => [], @@ -568,8 +568,8 @@ my $privgroups = { my $valid_privs = {}; my $special_roles = { - 'NoAccess' => {}, # no priviledges - 'Administrator' => $valid_privs, # all priviledges + 'NoAccess' => {}, # no privileges + 'Administrator' => $valid_privs, # all privileges }; sub create_roles { @@ -611,7 +611,7 @@ sub add_role_privs { if (defined ($valid_privs->{$priv})) { $usercfg->{roles}->{$role}->{$priv} = 1; } else { - die "invalid priviledge '$priv'\n"; + die "invalid privilege '$priv'\n"; } } } @@ -680,7 +680,7 @@ sub verify_privname { my ($priv, $noerr) = @_; if (!$valid_privs->{$priv}) { - die "invalid priviledge '$priv'\n" if !$noerr; + die "invalid privilege '$priv'\n" if !$noerr; return undef; } @@ -959,7 +959,7 @@ sub write_user_config { } foreach my $user (keys %{$d->{users}}) { - # no need to save, because root is always 'Administartor' + # no need to save, because root is always 'Administrator' next if $user eq 'root@pam'; my $l0 = ''; @@ -1173,6 +1173,23 @@ sub remove_vm_from_pool { lock_user_config($delVMfromPoolFn, "pool cleanup for VM $vmid failed"); } +# hotp/totp code + +sub hotp($$;$) { + my ($binsecret, $number, $digits) = @_; + + $digits = 6 if !defined($digits); + + my $bincounter = pack('Q>', $number); + my $hmac = Digest::SHA::hmac_sha1($bincounter, $binsecret); + + my $offset = unpack('C', substr($hmac,19) & pack('C', 0x0F)); + my $part = substr($hmac, $offset, 4); + my $otp = unpack('N', $part); + my $value = ($otp & 0x7fffffff) % (10**$digits); + return sprintf("%0${digits}d", $value); +} + # experimental code for yubico OTP verification sub yubico_compute_param_sig { @@ -1184,7 +1201,8 @@ sub yubico_compute_param_sig { $paramstr .= "$key=$param->{$key}"; } - my $sig = uri_escape(encode_base64(Digest::HMAC_SHA1::hmac_sha1($paramstr, decode_base64($api_key || '')), '')); + # hmac_sha1_base64 does not add '=' padding characters, so we use encode_base64 + my $sig = uri_escape(encode_base64(Digest::SHA::hmac_sha1($paramstr, decode_base64($api_key || '')), '')); return ($paramstr, $sig); } @@ -1197,15 +1215,12 @@ sub yubico_verify_otp { die "yubico: missing API KEY\n" if !defined($api_key); die "yubico: no associated yubico keys\n" if $keys =~ m/^\s+$/; - die "yubico: wrong OTP lenght\n" if (length($otp) < 32) || (length($otp) > 48); - - # we always use http, because https cert verification always make problem, and - # some proxies does not work with https. + die "yubico: wrong OTP length\n" if (length($otp) < 32) || (length($otp) > 48); $url = 'http://api2.yubico.com/wsapi/2.0/verify' if !defined($url); my $params = { - nonce => Digest::HMAC_SHA1::hmac_sha1_hex(time(), rand()), + nonce => Digest::SHA::hmac_sha1_hex(time(), rand()), id => $api_id, otp => uri_escape($otp), timestamp => 1, @@ -1217,10 +1232,10 @@ sub yubico_verify_otp { my $req = HTTP::Request->new('GET' => "$url?$paramstr"); - my $ua = LWP::UserAgent->new(protocols_allowed => ['http'], timeout => 30); + my $ua = LWP::UserAgent->new(protocols_allowed => ['http', 'https'], timeout => 30); if ($proxy) { - $ua->proxy(['http'], $proxy); + $ua->proxy(['http', 'https'], $proxy); } else { $ua->env_proxy; } @@ -1281,20 +1296,23 @@ sub oath_verify_otp { $digits = 6 if !$digits; my $found; - - my $parser = sub { - my $line = shift; - - if ($line =~ m/^\d{6}$/) { - $found = 1 if $otp eq $line; - } - }; - foreach my $k (PVE::Tools::split_list($keys)) { # Note: we generate 3 values to allow small time drift - my $now = localtime(time() - $step); - my $cmd = ['oathtool', '--totp', '--digits', $digits, '-N', $now, '-s', $step, '-w', '2', '-b', $k]; - eval { run_command($cmd, outfunc => $parser, errfunc => sub {}); }; + my $binkey; + if ($k =~ /^[A-Z2-7=]{16}$/) { + $binkey = MIME::Base32::decode_rfc3548($k); + } elsif ($k =~ /^[A-Fa-f0-9]{40}$/) { + $binkey = pack('H*', $k); + } else { + die "unrecognized key format, must be hex or base32 encoded\n"; + } + + # force integer division for time/step + use integer; + my $now = time()/$step - 1; + $found = 1 if $otp eq hotp($binkey, $now+0, $digits); + $found = 1 if $otp eq hotp($binkey, $now+1, $digits); + $found = 1 if $otp eq hotp($binkey, $now+2, $digits); last if $found; }