]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/Ticket.pm
subscription: handle missing subscription info
[pmg-api.git] / src / PMG / Ticket.pm
CommitLineData
1360e6f0
DM
1package PMG::Ticket;
2
3use strict;
4use warnings;
5use Net::SSLeay;
6use Digest::SHA;
7
3dbcd6a0 8use PVE::SafeSyslog;
9d7f54a3 9use PVE::Tools;
1360e6f0 10use PVE::Ticket;
bc44eb02 11use PVE::INotify;
1360e6f0
DM
12
13use Crypt::OpenSSL::RSA;
14
896ef634 15use PMG::Utils;
56efb270 16use PMG::Config;
896ef634 17
1360e6f0
DM
18my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift
19my $max_ticket_lifetime = 60*60*2; # 2 hours
20
3278b571 21my $basedir = "/etc/pmg";
d883fac2
DM
22
23my $pmg_api_cert_fn = "$basedir/pmg-api.pem";
24
1359baef 25# this is just a secret accessible by all API servers
d883fac2
DM
26# and is used for CSRF prevention
27my $pmg_csrf_key_fn = "$basedir/pmg-csrf.key";
9d7f54a3 28
c151b711
DM
29my $authprivkeyfn = "$basedir/pmg-authkey.key";
30my $authpubkeyfn = "$basedir/pmg-authkey.pub";
1360e6f0 31
9d7f54a3 32sub generate_api_cert {
bc44eb02
DM
33 my ($force) = @_;
34
35 my $nodename = PVE::INotify::nodename();
9d7f54a3
DM
36
37 if (-f $pmg_api_cert_fn) {
38 return $pmg_api_cert_fn if !$force;
39 unlink $pmg_api_cert_fn;
40 }
41
c151b711
DM
42 my $gid = getgrnam('www-data') ||
43 die "user www-data not in group file\n";
44
45 my $tmp_fn = "$pmg_api_cert_fn.tmp$$";
46
9d7f54a3 47 my $cmd = ['openssl', 'req', '-batch', '-x509', '-newkey', 'rsa:4096',
c151b711 48 '-nodes', '-keyout', $tmp_fn, '-out', $tmp_fn,
9d7f54a3
DM
49 '-subj', "/CN=$nodename/",
50 '-days', '3650'];
51
c151b711 52 eval {
896ef634 53 PMG::Utils::run_silent_cmd($cmd);
c151b711
DM
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";
57 };
58 if (my $err = $@) {
59 unlink $tmp_fn;
60 die "unable to generate pmg api cert '$pmg_api_cert_fn':\n$err";
61 }
9d7f54a3
DM
62
63 return $pmg_api_cert_fn;
64}
65
d883fac2
DM
66sub generate_csrf_key {
67
68 return if -f $pmg_csrf_key_fn;
69
c151b711
DM
70 my $gid = getgrnam('www-data') ||
71 die "user www-data not in group file\n";
d883fac2 72
c151b711
DM
73 my $tmp_fn = "$pmg_csrf_key_fn.tmp$$";
74 my $cmd = ['openssl', 'genrsa', '-out', $tmp_fn, '2048'];
75
76 eval {
896ef634 77 PMG::Utils::run_silent_cmd($cmd);
c151b711
DM
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";
81 };
82 if (my $err = $@) {
83 unlink $tmp_fn;
84 die "unable to generate pmg csrf key '$pmg_csrf_key_fn':\n$@";
85 }
d883fac2 86
c151b711
DM
87 return $pmg_csrf_key_fn;
88}
89
90sub generate_auth_key {
91
92 return if -f "$authprivkeyfn";
93
94 eval {
896ef634
DM
95 my $cmd = ['openssl', 'genrsa', '-out', $authprivkeyfn, '2048'];
96 PMG::Utils::run_silent_cmd($cmd);
c151b711 97
896ef634
DM
98 $cmd = ['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout',
99 '-out', $authpubkeyfn];
100 PMG::Utils::run_silent_cmd($cmd);
c151b711
DM
101 };
102
103 die "unable to generate pmg auth key:\n$@" if $@;
104}
105
3dbcd6a0
DM
106my $read_rsa_priv_key = sub {
107 my ($filename, $fh) = @_;
c151b711 108
3dbcd6a0 109 local $/ = undef; # slurp mode
c151b711 110
3dbcd6a0 111 my $input = <$fh>;
c151b711 112
3dbcd6a0 113 return Crypt::OpenSSL::RSA->new_private_key($input);
c151b711 114
3dbcd6a0 115};
c151b711 116
3dbcd6a0
DM
117PVE::INotify::register_file('auth_priv_key', $authprivkeyfn,
118 $read_rsa_priv_key, undef, undef,
119 noclone => 1);
c151b711 120
3dbcd6a0
DM
121my $read_rsa_pub_key = sub {
122 my ($filename, $fh) = @_;
c151b711 123
3dbcd6a0 124 local $/ = undef; # slurp mode
c151b711 125
3dbcd6a0 126 my $input = <$fh>;
c151b711 127
3dbcd6a0
DM
128 return Crypt::OpenSSL::RSA->new_public_key($input);
129};
d883fac2 130
3dbcd6a0
DM
131PVE::INotify::register_file('auth_pub_key', $authpubkeyfn,
132 $read_rsa_pub_key, undef, undef,
133 noclone => 1);
134
ba75b669 135my $read_csrf_secret = sub {
3dbcd6a0
DM
136 my ($filename, $fh) = @_;
137
138 local $/ = undef; # slurp mode
139
140 my $input = <$fh>;
141
a730a4fd 142 return Digest::SHA::hmac_sha256_base64($input);
1360e6f0
DM
143};
144
ba75b669
DM
145PVE::INotify::register_file('csrf_secret', $pmg_csrf_key_fn,
146 $read_csrf_secret, undef, undef,
3dbcd6a0 147 noclone => 1);
1360e6f0
DM
148
149sub verify_csrf_prevention_token {
150 my ($username, $token, $noerr) = @_;
151
ba75b669 152 my $secret = PVE::INotify::read_file('csrf_secret');
1360e6f0
DM
153
154 return PVE::Ticket::verify_csrf_prevention_token(
d883fac2 155 $secret, $username, $token, $min_ticket_lifetime,
1360e6f0
DM
156 $max_ticket_lifetime, $noerr);
157}
158
159sub assemble_csrf_prevention_token {
160 my ($username) = @_;
161
ba75b669 162 my $secret = PVE::INotify::read_file('csrf_secret');
1360e6f0
DM
163
164 return PVE::Ticket::assemble_csrf_prevention_token ($secret, $username);
165}
166
5accfdf2
WB
167sub assemble_ticket : prototype($;$) {
168 my ($data, $aad) = @_;
1360e6f0 169
3dbcd6a0 170 my $rsa_priv = PVE::INotify::read_file('auth_priv_key');
c151b711 171
5accfdf2 172 return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PMG', $data, $aad);
1360e6f0
DM
173}
174
5accfdf2
WB
175# Returns (username, age, tfa-challenge) or just the username in scalar context.
176# Note that in scalar context, tfa tickets return `undef`.
177sub verify_ticket : prototype($$$) {
178 my ($ticket, $aad, $noerr) = @_;
1360e6f0 179
3dbcd6a0 180 my $rsa_pub = PVE::INotify::read_file('auth_pub_key');
c151b711 181
5accfdf2
WB
182 my $tfa_challenge;
183 my ($data, $age) = PVE::Ticket::verify_rsa_ticket(
184 $rsa_pub, 'PMG', $ticket, $aad,
1360e6f0 185 $min_ticket_lifetime, $max_ticket_lifetime, $noerr);
5accfdf2
WB
186
187 if ($noerr && !$data) {
188 # if $noerr was set $data can be undef:
189 return wantarray ? (undef, undef, undef) : undef;
190 }
191
192
193 if ($data =~ /^!tfa!(.*)$/) {
194 return (undef, $age, $1) if wantarray;
195 return undef if $noerr;
196 die "second factor required\n";
197 }
198 return wantarray ? ($data, $age, undef) : $data;
1360e6f0
DM
199}
200
201# VNC tickets
202# - they do not contain the username in plain text
203# - they are restricted to a specific resource path (example: '/vms/100')
204sub assemble_vnc_ticket {
205 my ($username, $path) = @_;
206
3dbcd6a0 207 my $rsa_priv = PVE::INotify::read_file('auth_priv_key');
c151b711 208
1360e6f0
DM
209 my $secret_data = "$username:$path";
210
211 return PVE::Ticket::assemble_rsa_ticket(
c151b711 212 $rsa_priv, 'PMGVNC', undef, $secret_data);
1360e6f0
DM
213}
214
215sub verify_vnc_ticket {
216 my ($ticket, $username, $path, $noerr) = @_;
217
3dbcd6a0 218 my $rsa_pub = PVE::INotify::read_file('auth_pub_key');
c151b711 219
1360e6f0
DM
220 my $secret_data = "$username:$path";
221
222 return PVE::Ticket::verify_rsa_ticket(
c151b711 223 $rsa_pub, 'PMGVNC', $ticket, $secret_data, -20, 40, $noerr);
1360e6f0
DM
224}
225
6060e345
DM
226# Note: we only encode $pmail into the ticket,
227# and add '@quarantine' in verify_quarantine_ticket()
9a728eba 228sub assemble_quarantine_ticket {
f46af1cf 229 my ($pmail) = @_;
9a728eba
DM
230
231 my $rsa_priv = PVE::INotify::read_file('auth_priv_key');
232
f46af1cf 233 return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PMGQUAR', $pmail);
9a728eba
DM
234}
235
56efb270
DM
236my $quarantine_lifetime;
237
238my $get_quarantine_lifetime = sub {
239
240 return $quarantine_lifetime if defined($quarantine_lifetime);
241
242 my $cfg = PMG::Config->new();
243
244 $quarantine_lifetime = $cfg->get('spamquar', 'lifetime');
245
246 return $quarantine_lifetime;
247};
248
9a728eba 249sub verify_quarantine_ticket {
56efb270 250 my ($ticket, $noerr) = @_;
9a728eba
DM
251
252 my $rsa_pub = PVE::INotify::read_file('auth_pub_key');
253
56efb270
DM
254 my $lifetime = $get_quarantine_lifetime->();
255
6060e345 256 my ($username, $age) = PVE::Ticket::verify_rsa_ticket(
f46af1cf 257 $rsa_pub, 'PMGQUAR', $ticket, undef, -20, $lifetime*86400, $noerr);
6060e345
DM
258
259 $username = "$username\@quarantine" if defined($username);
260
261 return wantarray ? ($username, $age) : $username;
9a728eba
DM
262}
263
1360e6f0 2641;