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