]>
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 | |
181ef3f1 | 109 | my $input = do { local $/ = undef; <$fh> }; |
c151b711 | 110 | |
3dbcd6a0 | 111 | return Crypt::OpenSSL::RSA->new_private_key($input); |
c151b711 | 112 | |
3dbcd6a0 | 113 | }; |
c151b711 | 114 | |
3dbcd6a0 DM |
115 | PVE::INotify::register_file('auth_priv_key', $authprivkeyfn, |
116 | $read_rsa_priv_key, undef, undef, | |
117 | noclone => 1); | |
c151b711 | 118 | |
3dbcd6a0 DM |
119 | my $read_rsa_pub_key = sub { |
120 | my ($filename, $fh) = @_; | |
c151b711 | 121 | |
181ef3f1 | 122 | my $input = do { local $/ = undef; <$fh> }; |
c151b711 | 123 | |
3dbcd6a0 DM |
124 | return Crypt::OpenSSL::RSA->new_public_key($input); |
125 | }; | |
d883fac2 | 126 | |
3dbcd6a0 DM |
127 | PVE::INotify::register_file('auth_pub_key', $authpubkeyfn, |
128 | $read_rsa_pub_key, undef, undef, | |
129 | noclone => 1); | |
130 | ||
ba75b669 | 131 | my $read_csrf_secret = sub { |
3dbcd6a0 DM |
132 | my ($filename, $fh) = @_; |
133 | ||
181ef3f1 | 134 | my $input = do { local $/ = undef; <$fh> }; |
3dbcd6a0 | 135 | |
a730a4fd | 136 | return Digest::SHA::hmac_sha256_base64($input); |
1360e6f0 DM |
137 | }; |
138 | ||
ba75b669 DM |
139 | PVE::INotify::register_file('csrf_secret', $pmg_csrf_key_fn, |
140 | $read_csrf_secret, undef, undef, | |
3dbcd6a0 | 141 | noclone => 1); |
1360e6f0 DM |
142 | |
143 | sub verify_csrf_prevention_token { | |
144 | my ($username, $token, $noerr) = @_; | |
145 | ||
ba75b669 | 146 | my $secret = PVE::INotify::read_file('csrf_secret'); |
1360e6f0 DM |
147 | |
148 | return PVE::Ticket::verify_csrf_prevention_token( | |
d883fac2 | 149 | $secret, $username, $token, $min_ticket_lifetime, |
1360e6f0 DM |
150 | $max_ticket_lifetime, $noerr); |
151 | } | |
152 | ||
153 | sub assemble_csrf_prevention_token { | |
154 | my ($username) = @_; | |
155 | ||
ba75b669 | 156 | my $secret = PVE::INotify::read_file('csrf_secret'); |
1360e6f0 DM |
157 | |
158 | return PVE::Ticket::assemble_csrf_prevention_token ($secret, $username); | |
159 | } | |
160 | ||
5accfdf2 WB |
161 | sub assemble_ticket : prototype($;$) { |
162 | my ($data, $aad) = @_; | |
1360e6f0 | 163 | |
3dbcd6a0 | 164 | my $rsa_priv = PVE::INotify::read_file('auth_priv_key'); |
c151b711 | 165 | |
5accfdf2 | 166 | return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PMG', $data, $aad); |
1360e6f0 DM |
167 | } |
168 | ||
5accfdf2 WB |
169 | # Returns (username, age, tfa-challenge) or just the username in scalar context. |
170 | # Note that in scalar context, tfa tickets return `undef`. | |
171 | sub verify_ticket : prototype($$$) { | |
172 | my ($ticket, $aad, $noerr) = @_; | |
1360e6f0 | 173 | |
3dbcd6a0 | 174 | my $rsa_pub = PVE::INotify::read_file('auth_pub_key'); |
c151b711 | 175 | |
5accfdf2 WB |
176 | my $tfa_challenge; |
177 | my ($data, $age) = PVE::Ticket::verify_rsa_ticket( | |
178 | $rsa_pub, 'PMG', $ticket, $aad, | |
1360e6f0 | 179 | $min_ticket_lifetime, $max_ticket_lifetime, $noerr); |
5accfdf2 WB |
180 | |
181 | if ($noerr && !$data) { | |
182 | # if $noerr was set $data can be undef: | |
183 | return wantarray ? (undef, undef, undef) : undef; | |
184 | } | |
185 | ||
186 | ||
187 | if ($data =~ /^!tfa!(.*)$/) { | |
188 | return (undef, $age, $1) if wantarray; | |
189 | return undef if $noerr; | |
190 | die "second factor required\n"; | |
191 | } | |
192 | return wantarray ? ($data, $age, undef) : $data; | |
1360e6f0 DM |
193 | } |
194 | ||
195 | # VNC tickets | |
196 | # - they do not contain the username in plain text | |
197 | # - they are restricted to a specific resource path (example: '/vms/100') | |
198 | sub assemble_vnc_ticket { | |
199 | my ($username, $path) = @_; | |
200 | ||
3dbcd6a0 | 201 | my $rsa_priv = PVE::INotify::read_file('auth_priv_key'); |
c151b711 | 202 | |
1360e6f0 DM |
203 | my $secret_data = "$username:$path"; |
204 | ||
205 | return PVE::Ticket::assemble_rsa_ticket( | |
c151b711 | 206 | $rsa_priv, 'PMGVNC', undef, $secret_data); |
1360e6f0 DM |
207 | } |
208 | ||
209 | sub verify_vnc_ticket { | |
210 | my ($ticket, $username, $path, $noerr) = @_; | |
211 | ||
3dbcd6a0 | 212 | my $rsa_pub = PVE::INotify::read_file('auth_pub_key'); |
c151b711 | 213 | |
1360e6f0 DM |
214 | my $secret_data = "$username:$path"; |
215 | ||
216 | return PVE::Ticket::verify_rsa_ticket( | |
c151b711 | 217 | $rsa_pub, 'PMGVNC', $ticket, $secret_data, -20, 40, $noerr); |
1360e6f0 DM |
218 | } |
219 | ||
6060e345 DM |
220 | # Note: we only encode $pmail into the ticket, |
221 | # and add '@quarantine' in verify_quarantine_ticket() | |
9a728eba | 222 | sub assemble_quarantine_ticket { |
f46af1cf | 223 | my ($pmail) = @_; |
9a728eba DM |
224 | |
225 | my $rsa_priv = PVE::INotify::read_file('auth_priv_key'); | |
226 | ||
f46af1cf | 227 | return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PMGQUAR', $pmail); |
9a728eba DM |
228 | } |
229 | ||
56efb270 DM |
230 | my $quarantine_lifetime; |
231 | ||
232 | my $get_quarantine_lifetime = sub { | |
233 | ||
234 | return $quarantine_lifetime if defined($quarantine_lifetime); | |
235 | ||
236 | my $cfg = PMG::Config->new(); | |
237 | ||
238 | $quarantine_lifetime = $cfg->get('spamquar', 'lifetime'); | |
239 | ||
240 | return $quarantine_lifetime; | |
241 | }; | |
242 | ||
9a728eba | 243 | sub verify_quarantine_ticket { |
56efb270 | 244 | my ($ticket, $noerr) = @_; |
9a728eba DM |
245 | |
246 | my $rsa_pub = PVE::INotify::read_file('auth_pub_key'); | |
247 | ||
56efb270 DM |
248 | my $lifetime = $get_quarantine_lifetime->(); |
249 | ||
6060e345 | 250 | my ($username, $age) = PVE::Ticket::verify_rsa_ticket( |
f46af1cf | 251 | $rsa_pub, 'PMGQUAR', $ticket, undef, -20, $lifetime*86400, $noerr); |
6060e345 DM |
252 | |
253 | $username = "$username\@quarantine" if defined($username); | |
254 | ||
255 | return wantarray ? ($username, $age) : $username; | |
9a728eba DM |
256 | } |
257 | ||
1360e6f0 | 258 | 1; |