13 use Crypt
::OpenSSL
::RSA
;
18 my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift
19 my $max_ticket_lifetime = 60*60*2; # 2 hours
21 my $basedir = "/etc/pmg";
23 my $pmg_api_cert_fn = "$basedir/pmg-api.pem";
25 # this is just a secret accessible by all API servers
26 # and is used for CSRF prevention
27 my $pmg_csrf_key_fn = "$basedir/pmg-csrf.key";
29 my $authprivkeyfn = "$basedir/pmg-authkey.key";
30 my $authpubkeyfn = "$basedir/pmg-authkey.pub";
32 sub generate_api_cert
{
35 my $nodename = PVE
::INotify
::nodename
();
37 if (-f
$pmg_api_cert_fn) {
38 return $pmg_api_cert_fn if !$force;
39 unlink $pmg_api_cert_fn;
42 my $gid = getgrnam('www-data') ||
43 die "user www-data not in group file\n";
45 my $tmp_fn = "$pmg_api_cert_fn.tmp$$";
47 my $cmd = ['openssl', 'req', '-batch', '-x509', '-newkey', 'rsa:4096',
48 '-nodes', '-keyout', $tmp_fn, '-out', $tmp_fn,
49 '-subj', "/CN=$nodename/",
53 PMG
::Utils
::run_silent_cmd
($cmd);
54 chown(0, $gid, $tmp_fn) || die "chown failed - $!\n";
55 chmod(0640, $tmp_fn) || die "chmod failed - $!\n";
56 rename($tmp_fn, $pmg_api_cert_fn) || die "rename failed - $!\n";
60 die "unable to generate pmg api cert '$pmg_api_cert_fn':\n$err";
63 return $pmg_api_cert_fn;
66 sub generate_csrf_key
{
68 return if -f
$pmg_csrf_key_fn;
70 my $gid = getgrnam('www-data') ||
71 die "user www-data not in group file\n";
73 my $tmp_fn = "$pmg_csrf_key_fn.tmp$$";
74 my $cmd = ['openssl', 'genrsa', '-out', $tmp_fn, '2048'];
77 PMG
::Utils
::run_silent_cmd
($cmd);
78 chown(0, $gid, $tmp_fn) || die "chown failed - $!\n";
79 chmod(0640, $tmp_fn) || die "chmod failed - $!\n";
80 rename($tmp_fn, $pmg_csrf_key_fn) || die "rename failed - $!\n";
84 die "unable to generate pmg csrf key '$pmg_csrf_key_fn':\n$@";
87 return $pmg_csrf_key_fn;
90 sub generate_auth_key
{
92 return if -f
"$authprivkeyfn";
95 my $cmd = ['openssl', 'genrsa', '-out', $authprivkeyfn, '2048'];
96 PMG
::Utils
::run_silent_cmd
($cmd);
98 $cmd = ['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout',
99 '-out', $authpubkeyfn];
100 PMG
::Utils
::run_silent_cmd
($cmd);
103 die "unable to generate pmg auth key:\n$@" if $@;
106 my $read_rsa_priv_key = sub {
107 my ($filename, $fh) = @_;
109 local $/ = undef; # slurp mode
113 return Crypt
::OpenSSL
::RSA-
>new_private_key($input);
117 PVE
::INotify
::register_file
('auth_priv_key', $authprivkeyfn,
118 $read_rsa_priv_key, undef, undef,
121 my $read_rsa_pub_key = sub {
122 my ($filename, $fh) = @_;
124 local $/ = undef; # slurp mode
128 return Crypt
::OpenSSL
::RSA-
>new_public_key($input);
131 PVE
::INotify
::register_file
('auth_pub_key', $authpubkeyfn,
132 $read_rsa_pub_key, undef, undef,
135 my $read_csrf_secret = sub {
136 my ($filename, $fh) = @_;
138 local $/ = undef; # slurp mode
142 return Digest
::SHA
::hmac_sha256_base64
($input);
145 PVE
::INotify
::register_file
('csrf_secret', $pmg_csrf_key_fn,
146 $read_csrf_secret, undef, undef,
149 sub verify_csrf_prevention_token
{
150 my ($username, $token, $noerr) = @_;
152 my $secret = PVE
::INotify
::read_file
('csrf_secret');
154 return PVE
::Ticket
::verify_csrf_prevention_token
(
155 $secret, $username, $token, $min_ticket_lifetime,
156 $max_ticket_lifetime, $noerr);
159 sub assemble_csrf_prevention_token
{
162 my $secret = PVE
::INotify
::read_file
('csrf_secret');
164 return PVE
::Ticket
::assemble_csrf_prevention_token
($secret, $username);
167 sub assemble_ticket
: prototype($;$) {
168 my ($data, $aad) = @_;
170 my $rsa_priv = PVE
::INotify
::read_file
('auth_priv_key');
172 return PVE
::Ticket
::assemble_rsa_ticket
($rsa_priv, 'PMG', $data, $aad);
175 # Returns (username, age, tfa-challenge) or just the username in scalar context.
176 # Note that in scalar context, tfa tickets return `undef`.
177 sub verify_ticket
: prototype($$$) {
178 my ($ticket, $aad, $noerr) = @_;
180 my $rsa_pub = PVE
::INotify
::read_file
('auth_pub_key');
183 my ($data, $age) = PVE
::Ticket
::verify_rsa_ticket
(
184 $rsa_pub, 'PMG', $ticket, $aad,
185 $min_ticket_lifetime, $max_ticket_lifetime, $noerr);
187 if ($noerr && !$data) {
188 # if $noerr was set $data can be undef:
189 return wantarray ?
(undef, undef, undef) : undef;
193 if ($data =~ /^!tfa!(.*)$/) {
194 return (undef, $age, $1) if wantarray;
195 return undef if $noerr;
196 die "second factor required\n";
198 return wantarray ?
($data, $age, undef) : $data;
202 # - they do not contain the username in plain text
203 # - they are restricted to a specific resource path (example: '/vms/100')
204 sub assemble_vnc_ticket
{
205 my ($username, $path) = @_;
207 my $rsa_priv = PVE
::INotify
::read_file
('auth_priv_key');
209 my $secret_data = "$username:$path";
211 return PVE
::Ticket
::assemble_rsa_ticket
(
212 $rsa_priv, 'PMGVNC', undef, $secret_data);
215 sub verify_vnc_ticket
{
216 my ($ticket, $username, $path, $noerr) = @_;
218 my $rsa_pub = PVE
::INotify
::read_file
('auth_pub_key');
220 my $secret_data = "$username:$path";
222 return PVE
::Ticket
::verify_rsa_ticket
(
223 $rsa_pub, 'PMGVNC', $ticket, $secret_data, -20, 40, $noerr);
226 # Note: we only encode $pmail into the ticket,
227 # and add '@quarantine' in verify_quarantine_ticket()
228 sub assemble_quarantine_ticket
{
231 my $rsa_priv = PVE
::INotify
::read_file
('auth_priv_key');
233 return PVE
::Ticket
::assemble_rsa_ticket
($rsa_priv, 'PMGQUAR', $pmail);
236 my $quarantine_lifetime;
238 my $get_quarantine_lifetime = sub {
240 return $quarantine_lifetime if defined($quarantine_lifetime);
242 my $cfg = PMG
::Config-
>new();
244 $quarantine_lifetime = $cfg->get('spamquar', 'lifetime');
246 return $quarantine_lifetime;
249 sub verify_quarantine_ticket
{
250 my ($ticket, $noerr) = @_;
252 my $rsa_pub = PVE
::INotify
::read_file
('auth_pub_key');
254 my $lifetime = $get_quarantine_lifetime->();
256 my ($username, $age) = PVE
::Ticket
::verify_rsa_ticket
(
257 $rsa_pub, 'PMGQUAR', $ticket, undef, -20, $lifetime*86400, $noerr);
259 $username = "$username\@quarantine" if defined($username);
261 return wantarray ?
($username, $age) : $username;