]>
Commit | Line | Data |
---|---|---|
1360e6f0 DM |
1 | package PMG::Ticket; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use Net::SSLeay; | |
6 | use Digest::SHA; | |
7 | ||
3dbcd6a0 | 8 | use PVE::SafeSyslog; |
9d7f54a3 | 9 | use PVE::Tools; |
1360e6f0 | 10 | use PVE::Ticket; |
bc44eb02 | 11 | use PVE::INotify; |
1360e6f0 DM |
12 | |
13 | use Crypt::OpenSSL::RSA; | |
14 | ||
896ef634 | 15 | use PMG::Utils; |
56efb270 | 16 | use PMG::Config; |
896ef634 | 17 | |
1360e6f0 DM |
18 | my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift |
19 | my $max_ticket_lifetime = 60*60*2; # 2 hours | |
20 | ||
3278b571 | 21 | my $basedir = "/etc/pmg"; |
d883fac2 DM |
22 | |
23 | my $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 |
27 | my $pmg_csrf_key_fn = "$basedir/pmg-csrf.key"; | |
9d7f54a3 | 28 | |
c151b711 DM |
29 | my $authprivkeyfn = "$basedir/pmg-authkey.key"; |
30 | my $authpubkeyfn = "$basedir/pmg-authkey.pub"; | |
1360e6f0 | 31 | |
9d7f54a3 | 32 | sub 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 |
66 | sub 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 | ||
90 | sub 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 |
106 | my $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 |
117 | PVE::INotify::register_file('auth_priv_key', $authprivkeyfn, |
118 | $read_rsa_priv_key, undef, undef, | |
119 | noclone => 1); | |
c151b711 | 120 | |
3dbcd6a0 DM |
121 | my $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 |
131 | PVE::INotify::register_file('auth_pub_key', $authpubkeyfn, |
132 | $read_rsa_pub_key, undef, undef, | |
133 | noclone => 1); | |
134 | ||
ba75b669 | 135 | my $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 |
145 | PVE::INotify::register_file('csrf_secret', $pmg_csrf_key_fn, |
146 | $read_csrf_secret, undef, undef, | |
3dbcd6a0 | 147 | noclone => 1); |
1360e6f0 DM |
148 | |
149 | sub 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 | ||
159 | sub 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 | ||
167 | sub assemble_ticket { | |
168 | my ($username) = @_; | |
169 | ||
3dbcd6a0 | 170 | my $rsa_priv = PVE::INotify::read_file('auth_priv_key'); |
c151b711 DM |
171 | |
172 | return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PMG', $username); | |
1360e6f0 DM |
173 | } |
174 | ||
175 | sub verify_ticket { | |
176 | my ($ticket, $noerr) = @_; | |
177 | ||
3dbcd6a0 | 178 | my $rsa_pub = PVE::INotify::read_file('auth_pub_key'); |
c151b711 | 179 | |
1360e6f0 | 180 | return PVE::Ticket::verify_rsa_ticket( |
c151b711 | 181 | $rsa_pub, 'PMG', $ticket, undef, |
1360e6f0 DM |
182 | $min_ticket_lifetime, $max_ticket_lifetime, $noerr); |
183 | } | |
184 | ||
185 | # VNC tickets | |
186 | # - they do not contain the username in plain text | |
187 | # - they are restricted to a specific resource path (example: '/vms/100') | |
188 | sub assemble_vnc_ticket { | |
189 | my ($username, $path) = @_; | |
190 | ||
3dbcd6a0 | 191 | my $rsa_priv = PVE::INotify::read_file('auth_priv_key'); |
c151b711 | 192 | |
1360e6f0 DM |
193 | my $secret_data = "$username:$path"; |
194 | ||
195 | return PVE::Ticket::assemble_rsa_ticket( | |
c151b711 | 196 | $rsa_priv, 'PMGVNC', undef, $secret_data); |
1360e6f0 DM |
197 | } |
198 | ||
199 | sub verify_vnc_ticket { | |
200 | my ($ticket, $username, $path, $noerr) = @_; | |
201 | ||
3dbcd6a0 | 202 | my $rsa_pub = PVE::INotify::read_file('auth_pub_key'); |
c151b711 | 203 | |
1360e6f0 DM |
204 | my $secret_data = "$username:$path"; |
205 | ||
206 | return PVE::Ticket::verify_rsa_ticket( | |
c151b711 | 207 | $rsa_pub, 'PMGVNC', $ticket, $secret_data, -20, 40, $noerr); |
1360e6f0 DM |
208 | } |
209 | ||
6060e345 DM |
210 | # Note: we only encode $pmail into the ticket, |
211 | # and add '@quarantine' in verify_quarantine_ticket() | |
9a728eba | 212 | sub assemble_quarantine_ticket { |
f46af1cf | 213 | my ($pmail) = @_; |
9a728eba DM |
214 | |
215 | my $rsa_priv = PVE::INotify::read_file('auth_priv_key'); | |
216 | ||
f46af1cf | 217 | return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PMGQUAR', $pmail); |
9a728eba DM |
218 | } |
219 | ||
56efb270 DM |
220 | my $quarantine_lifetime; |
221 | ||
222 | my $get_quarantine_lifetime = sub { | |
223 | ||
224 | return $quarantine_lifetime if defined($quarantine_lifetime); | |
225 | ||
226 | my $cfg = PMG::Config->new(); | |
227 | ||
228 | $quarantine_lifetime = $cfg->get('spamquar', 'lifetime'); | |
229 | ||
230 | return $quarantine_lifetime; | |
231 | }; | |
232 | ||
9a728eba | 233 | sub verify_quarantine_ticket { |
56efb270 | 234 | my ($ticket, $noerr) = @_; |
9a728eba DM |
235 | |
236 | my $rsa_pub = PVE::INotify::read_file('auth_pub_key'); | |
237 | ||
56efb270 DM |
238 | my $lifetime = $get_quarantine_lifetime->(); |
239 | ||
6060e345 | 240 | my ($username, $age) = PVE::Ticket::verify_rsa_ticket( |
f46af1cf | 241 | $rsa_pub, 'PMGQUAR', $ticket, undef, -20, $lifetime*86400, $noerr); |
6060e345 DM |
242 | |
243 | $username = "$username\@quarantine" if defined($username); | |
244 | ||
245 | return wantarray ? ($username, $age) : $username; | |
9a728eba DM |
246 | } |
247 | ||
1360e6f0 | 248 | 1; |